<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>HaNong</title>
    <link>https://hanong8.tistory.com/</link>
    <description>hanong8 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 15 May 2026 07:45:13 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hanong8</managingEditor>
    <item>
      <title>[C++] 코딩테스트에서의 스택</title>
      <link>https://hanong8.tistory.com/104</link>
      <description>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot; /&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
&lt;link href=&quot;https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap&quot; rel=&quot;stylesheet&quot; /&gt;
&lt;style&gt;
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

  body {
    background: #1a1d23;
    color: #d4d8e2;
    font-family: 'Noto Sans KR', sans-serif;
    font-size: 15px;
    line-height: 1.8;
    padding: 40px 20px 80px;
  }

  .wrap {
    max-width: 760px;
    margin: 0 auto;
  }

  /* ── 섹션 타이틀 ── */
  .section-title {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 18px;
    font-weight: 700;
    color: #eef0f6;
    margin: 44px 0 20px;
  }
  .section-num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 6px;
    background: #1e3a5f;
    color: #4da3ff;
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px;
    font-weight: 600;
    flex-shrink: 0;
  }

  hr {
    border: none;
    border-top: 1px solid #2a2f3d;
    margin: 40px 0 0;
  }

  /* ── 본문 ── */
  p {
    color: #c8cdd8;
    margin-bottom: 14px;
  }
  strong { color: #eef0f6; font-weight: 600; }
  code {
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px;
    background: #252a36;
    color: #7eb8f7;
    padding: 2px 6px;
    border-radius: 4px;
  }

  /* ── 표 ── */
  table {
    width: 100%;
    border-collapse: collapse;
    margin: 18px 0 24px;
    font-size: 14px;
  }
  th {
    background: #252a36;
    color: #7a8394;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    font-weight: 600;
    padding: 10px 14px;
    text-align: left;
    letter-spacing: 0.03em;
  }
  td {
    padding: 10px 14px;
    border-bottom: 1px solid rgba(46,52,68,0.7);
    color: #c8cdd8;
    vertical-align: top;
  }
  tr:last-child td { border-bottom: none; }
  tr:hover td { background: rgba(255,255,255,0.025); }

  /* ── 회고 표 ── */
  .retrospect th {
    width: 130px;
    background: #1e2230;
  }
  .retrospect td:first-child {
    font-weight: 600;
    color: #eef0f6;
    white-space: nowrap;
    background: #1e2230;
  }

  /* ── step-list ── */
  .step-list { margin: 14px 0 20px; display: flex; flex-direction: column; gap: 10px; }
  .step-item { display: flex; align-items: flex-start; gap: 10px; }
  .step-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: #1e3a5f;
    color: #4da3ff;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    font-weight: 600;
    flex-shrink: 0;
    margin-top: 2px;
  }

  /* ── 코드 블록 ── */
  .code-wrap {
    background: #0e1117;
    border-radius: 10px;
    overflow: hidden;
    margin: 16px 0 24px;
    border: 1px solid #1e2230;
  }
  .code-header {
    background: #161b27;
    padding: 9px 14px;
    display: flex;
    align-items: center;
    gap: 10px;
    border-bottom: 1px solid #1e2230;
  }
  .code-dots { display: flex; gap: 6px; }
  .code-dots span {
    display: block;
    width: 12px;
    height: 12px;
    border-radius: 50%;
  }
  .dot-r { background: #ff5f57; }
  .dot-y { background: #febc2e; }
  .dot-g { background: #28c840; }
  .code-lang {
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    color: #4a5168;
    margin-left: 4px;
  }
  pre {
    padding: 18px 20px;
    overflow-x: auto;
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px;
    line-height: 1.75;
    color: #abb2bf;
  }
  .kw  { color: #c678dd; }
  .fn  { color: #61afef; }
  .tp  { color: #e5c07b; }
  .num { color: #d19a66; }
  .cm  { color: #5c6370; font-style: italic; }
  .str { color: #98c379; }

  /* ── note / warn ── */
  .note {
    background: rgba(30,58,95,0.45);
    border-left: 3px solid #4da3ff;
    border-radius: 0 8px 8px 0;
    padding: 14px 18px;
    margin: 14px 0 22px;
    color: #b8cfe8;
    font-size: 14px;
  }
  .note strong { color: #7eb8f7; }

  .warn-box {
    background: rgba(95,75,20,0.35);
    border-left: 3px solid #e5b800;
    border-radius: 0 8px 8px 0;
    padding: 14px 18px;
    margin: 14px 0 22px;
    color: #d4c27a;
    font-size: 14px;
  }
  .warn-box strong { color: #f0d060; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;wrap&quot;&gt;

  &lt;!-- ══════════════════════════════════════════
       섹션 1 : 스택의 특징과 LIFO 원리
  ══════════════════════════════════════════ --&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;1&lt;/span&gt;스택의 특징과 LIFO 원리
  &lt;/div&gt;

  &lt;p&gt;
    스택(Stack)은 &lt;strong&gt;가장 최근에 삽입한 원소가 가장 먼저 나오는&lt;/strong&gt;
    &lt;strong&gt;LIFO(Last In First Out)&lt;/strong&gt; 구조의 자료구조다.
    데이터를 쌓아 올리는 접시 더미처럼, 꺼낼 때는 맨 위부터 꺼낸다.
  &lt;/p&gt;
  &lt;p&gt;
    코딩테스트에서 스택이 등장하는 패턴은 대부분
    &lt;strong&gt;&quot;가장 최근에 넣은 원소&quot;&lt;/strong&gt;를 즉시 참조하거나 제거해야 하는 상황이다.
    괄호 검증, 단조 스택(Monotone Stack), 함수 콜스택 시뮬레이션 등이 대표적이다.
  &lt;/p&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;구조&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;th&gt;대표 자료구조&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;LIFO&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;마지막에 넣은 원소가 먼저 나옴&lt;/td&gt;
        &lt;td&gt;Stack&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;FIFO&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;먼저 넣은 원소가 먼저 나옴&lt;/td&gt;
        &lt;td&gt;Queue&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;hr /&gt;

  &lt;!-- ══════════════════════════════════════════
       섹션 2 : 스택의 활용 예시
  ══════════════════════════════════════════ --&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;2&lt;/span&gt;스택의 활용 예시
  &lt;/div&gt;

  &lt;p&gt;
    스택은 이론 속 개념이 아니라 우리가 매일 쓰는 기능에 이미 녹아 있다.
    두 가지 대표 예시를 코드와 함께 살펴보자.
  &lt;/p&gt;

  &lt;p&gt;&lt;strong&gt;예시 1 — 웹 브라우저 뒤로 가기&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;
    방문한 페이지를 스택에 순서대로 &lt;code&gt;push&lt;/code&gt;하고,
    뒤로 가기를 누르면 &lt;code&gt;pop&lt;/code&gt;하여 직전 페이지로 돌아간다.
    LIFO 구조이므로 가장 최근에 방문한 페이지가 자연스럽게 제거된다.
  &lt;/p&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;dot-r&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-y&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-g&quot;&gt;&lt;/span&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ — 뒤로 가기 기능 구현&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;cm&quot;&gt;// 방문한 페이지를 스택에 저장하고, 뒤로 가기 시 최근 페이지를 제거&lt;/span&gt;
&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;iostream&amp;gt;
&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;stack&amp;gt;
&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;string&amp;gt;
&lt;span class=&quot;kw&quot;&gt;using namespace&lt;/span&gt; std;

&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;fn&quot;&gt;main&lt;/span&gt;() {
    stack&amp;lt;string&amp;gt; history;

    history.&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(&lt;span class=&quot;str&quot;&gt;&quot;A&quot;&lt;/span&gt;);
    history.&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(&lt;span class=&quot;str&quot;&gt;&quot;B&quot;&lt;/span&gt;);
    history.&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(&lt;span class=&quot;str&quot;&gt;&quot;C&quot;&lt;/span&gt;);

    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;현재 페이지: &quot;&lt;/span&gt; &amp;lt;&amp;lt; history.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;() &amp;lt;&amp;lt; endl; &lt;span class=&quot;cm&quot;&gt;// C&lt;/span&gt;

    history.&lt;span class=&quot;fn&quot;&gt;pop&lt;/span&gt;(); &lt;span class=&quot;cm&quot;&gt;// C → B&lt;/span&gt;
    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;뒤로 가기 후 현재 페이지: &quot;&lt;/span&gt; &amp;lt;&amp;lt; history.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;() &amp;lt;&amp;lt; endl; &lt;span class=&quot;cm&quot;&gt;// B&lt;/span&gt;

    &lt;span class=&quot;kw&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;;
}
&lt;span class=&quot;cm&quot;&gt;/* 출력결과
현재 페이지: C
뒤로 가기 후 현재 페이지: B */&lt;/span&gt;&lt;/pre&gt;
  &lt;/div&gt;

  &lt;p&gt;&lt;strong&gt;예시 2 — 함수 호출 스택&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;
    C++ 프로그램이 실행될 때 함수 호출은 내부적으로 &lt;strong&gt;콜 스택(Call Stack)&lt;/strong&gt;으로 관리된다.
    &lt;code&gt;main&lt;/code&gt;이 &lt;code&gt;A&lt;/code&gt;를 호출하고, &lt;code&gt;A&lt;/code&gt;가 &lt;code&gt;B&lt;/code&gt;를 호출하면
    스택에 순서대로 쌓인다. &lt;code&gt;B&lt;/code&gt;가 종료되면 LIFO 구조에 따라 &lt;code&gt;A&lt;/code&gt;로 자연스럽게 복귀한다.
  &lt;/p&gt;

  &lt;div class=&quot;step-list&quot;&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;1&lt;/span&gt;
      &lt;span&gt;&lt;code&gt;main&lt;/code&gt; 함수에서 &lt;code&gt;A&lt;/code&gt; 함수 호출 → 스택에 main, A 순으로 적재&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;2&lt;/span&gt;
      &lt;span&gt;&lt;code&gt;A&lt;/code&gt; 함수에서 &lt;code&gt;B&lt;/code&gt; 함수 호출 → 스택에 main, A, B 순으로 적재&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;3&lt;/span&gt;
      &lt;span&gt;&lt;code&gt;B&lt;/code&gt; 함수 종료 → &lt;code&gt;B&lt;/code&gt;가 pop되고 자신을 호출한 &lt;code&gt;A&lt;/code&gt;로 반환&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;dot-r&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-y&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-g&quot;&gt;&lt;/span&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ — 함수 호출 스택 시뮬레이션&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;iostream&amp;gt;
&lt;span class=&quot;kw&quot;&gt;using namespace&lt;/span&gt; std;

&lt;span class=&quot;tp&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fn&quot;&gt;B&lt;/span&gt;() {
    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;B 함수 호출됨&quot;&lt;/span&gt; &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;B 함수 종료됨&quot;&lt;/span&gt; &amp;lt;&amp;lt; endl;
}
&lt;span class=&quot;tp&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fn&quot;&gt;A&lt;/span&gt;() {
    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;A 함수 호출됨&quot;&lt;/span&gt; &amp;lt;&amp;lt; endl;
    &lt;span class=&quot;fn&quot;&gt;B&lt;/span&gt;();
    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;A 함수 종료됨&quot;&lt;/span&gt; &amp;lt;&amp;lt; endl;
}
&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;fn&quot;&gt;main&lt;/span&gt;() {
    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;main 함수 호출됨&quot;&lt;/span&gt; &amp;lt;&amp;lt; endl;
    &lt;span class=&quot;fn&quot;&gt;A&lt;/span&gt;();
    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;main 함수 종료됨&quot;&lt;/span&gt; &amp;lt;&amp;lt; endl;
    &lt;span class=&quot;kw&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;;
}
&lt;span class=&quot;cm&quot;&gt;/* 출력결과
main 함수 호출됨 → A 함수 호출됨 → B 함수 호출됨
→ B 함수 종료됨 → A 함수 종료됨 → main 함수 종료됨 */&lt;/span&gt;&lt;/pre&gt;
  &lt;/div&gt;

  &lt;hr /&gt;

  &lt;!-- ══════════════════════════════════════════
       섹션 3 : 스택의 ADT 정의
  ══════════════════════════════════════════ --&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;3&lt;/span&gt;스택의 ADT 정의
  &lt;/div&gt;

  &lt;p&gt;
    &lt;strong&gt;ADT(Abstract Data Type, 추상 자료형)&lt;/strong&gt;란 자료구조의 동작을
    구체적인 구현과 분리하여 &lt;strong&gt;인터페이스 수준&lt;/strong&gt;에서 정의한 설계도다.
    실제로 배열로 구현하든 연결 리스트로 구현하든 외부에서는 동일하게 사용할 수 있다.
  &lt;/p&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;구분&lt;/th&gt;&lt;th&gt;정의&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;연산&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;boolean isFull()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;데이터 개수가 maxsize와 같으면 &lt;code&gt;true&lt;/code&gt;, 아니면 &lt;code&gt;false&lt;/code&gt; 반환&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;연산&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;boolean isEmpty()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;데이터가 하나도 없으면 &lt;code&gt;true&lt;/code&gt;, 있으면 &lt;code&gt;false&lt;/code&gt; 반환&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;연산&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;void push(ItemType item)&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;스택 top에 데이터를 추가&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;연산&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;ItemType pop()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;가장 최근에 추가된 데이터를 제거하고 반환&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;상태&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;int top&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;가장 최근에 추가된 데이터의 위치 인덱스&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;상태&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;ItemType data[maxsize]&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;스택의 데이터를 관리하는 배열, 최대 maxsize개 저장&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;hr /&gt;

  &lt;!-- ══════════════════════════════════════════
       섹션 4 : push / pop 동작 원리
  ══════════════════════════════════════════ --&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;4&lt;/span&gt;push / pop 동작 원리
  &lt;/div&gt;

  &lt;p&gt;ADT를 기준으로 &lt;code&gt;push&lt;/code&gt;와 &lt;code&gt;pop&lt;/code&gt;이 내부에서 어떻게 수행되는지 단계별로 확인한다.&lt;/p&gt;

  &lt;p&gt;&lt;strong&gt;push(3) 수행 과정&lt;/strong&gt;&lt;/p&gt;
  &lt;div class=&quot;step-list&quot;&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;1&lt;/span&gt;
      &lt;span&gt;&lt;code&gt;isFull()&lt;/code&gt;을 호출하여 스택이 가득 찼는지 확인&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;2&lt;/span&gt;
      &lt;span&gt;스택이 가득 차지 않은 경우 &lt;code&gt;top&lt;/code&gt;을 1 증가 (&lt;code&gt;-1 → 0&lt;/code&gt;)&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;3&lt;/span&gt;
      &lt;span&gt;&lt;code&gt;data[top]&lt;/code&gt;에 해당하는 위치에 값 3을 저장&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;p&gt;&lt;strong&gt;pop() 수행 과정&lt;/strong&gt;&lt;/p&gt;
  &lt;div class=&quot;step-list&quot;&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;1&lt;/span&gt;
      &lt;span&gt;&lt;code&gt;isEmpty()&lt;/code&gt;를 호출하여 빈 스택인지 확인&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;2&lt;/span&gt;
      &lt;span&gt;스택이 비어있지 않은 경우 &lt;code&gt;top&lt;/code&gt;을 1 감소 (&lt;code&gt;0 → -1&lt;/code&gt;)&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;3&lt;/span&gt;
      &lt;span&gt;제거된 위치의 데이터(3)를 반환 — 이것이 가장 최근에 넣은 원소&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;note&quot;&gt;
    &lt;strong&gt;핵심 포인트&lt;/strong&gt; — &lt;code&gt;top&lt;/code&gt;은 실제로 데이터를 지우지 않는다.
    인덱스를 감소시켜 논리적으로 비어있는 것으로 간주할 뿐이며,
    다음 &lt;code&gt;push&lt;/code&gt;가 그 자리를 덮어쓴다.
  &lt;/div&gt;

  &lt;hr /&gt;

  &lt;!-- ══════════════════════════════════════════
       섹션 5 : STL stack 주요 메서드
  ══════════════════════════════════════════ --&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;5&lt;/span&gt;STL stack 주요 메서드
  &lt;/div&gt;

  &lt;p&gt;
    C++은 &lt;code&gt;&amp;lt;stack&amp;gt;&lt;/code&gt; 헤더를 통해 스택을 기본 제공하므로 직접 구현할 필요가 없다.
    주요 메서드와 시간 복잡도를 정리하면 아래와 같다.
  &lt;/p&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;메서드&lt;/th&gt;&lt;th&gt;동작&lt;/th&gt;&lt;th&gt;예시&lt;/th&gt;&lt;th&gt;시간 복잡도&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;code&gt;top()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;top 요소 접근 (제거 안 함)&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;s.top()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;O(1)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;code&gt;push(val)&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;top에 요소 삽입&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;s.push(10)&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;O(1)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;code&gt;pop()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;top 요소 제거 (반환값 없음)&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;s.pop()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;O(1)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;code&gt;empty()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;스택이 비어 있는지 확인&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;s.empty()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;O(1)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;code&gt;size()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;스택 내 요소 개수 반환&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;s.size()&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;O(1)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;code&gt;push_range(first, last)&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;반복자 범위의 요소를 일괄 삽입&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;s.push_range(v.begin(), v.end())&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;O(N)&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;div class=&quot;warn-box&quot;&gt;
    &lt;strong&gt;주의 — pop()은 반환값이 없다.&lt;/strong&gt;&lt;br /&gt;
    값을 꺼내야 한다면 반드시 &lt;code&gt;top()&lt;/code&gt;으로 먼저 읽은 뒤 &lt;code&gt;pop()&lt;/code&gt;을 호출해야 한다.
    &lt;code&gt;int x = s.pop();&lt;/code&gt;은 컴파일 에러가 발생한다.
  &lt;/div&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;dot-r&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-y&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-g&quot;&gt;&lt;/span&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ — STL stack 기본 사용법&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;iostream&amp;gt;
&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;stack&amp;gt;
&lt;span class=&quot;kw&quot;&gt;using namespace&lt;/span&gt; std;

&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;fn&quot;&gt;main&lt;/span&gt;() {
    stack&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt; s;
    s.&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(&lt;span class=&quot;num&quot;&gt;10&lt;/span&gt;); s.&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(&lt;span class=&quot;num&quot;&gt;20&lt;/span&gt;); s.&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(&lt;span class=&quot;num&quot;&gt;30&lt;/span&gt;);

    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;스택의 top: &quot;&lt;/span&gt; &amp;lt;&amp;lt; s.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;() &amp;lt;&amp;lt; endl;  &lt;span class=&quot;cm&quot;&gt;// 30&lt;/span&gt;
    s.&lt;span class=&quot;fn&quot;&gt;pop&lt;/span&gt;();
    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;pop 후 top: &quot;&lt;/span&gt; &amp;lt;&amp;lt; s.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;() &amp;lt;&amp;lt; endl;  &lt;span class=&quot;cm&quot;&gt;// 20&lt;/span&gt;

    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;비어있나? &quot;&lt;/span&gt; &amp;lt;&amp;lt; (s.&lt;span class=&quot;fn&quot;&gt;empty&lt;/span&gt;() ? &lt;span class=&quot;str&quot;&gt;&quot;예&quot;&lt;/span&gt; : &lt;span class=&quot;str&quot;&gt;&quot;아니오&quot;&lt;/span&gt;) &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &lt;span class=&quot;str&quot;&gt;&quot;크기: &quot;&lt;/span&gt; &amp;lt;&amp;lt; s.&lt;span class=&quot;fn&quot;&gt;size&lt;/span&gt;() &amp;lt;&amp;lt; endl;  &lt;span class=&quot;cm&quot;&gt;// 2&lt;/span&gt;

    &lt;span class=&quot;cm&quot;&gt;// 올바른 값 추출 패턴&lt;/span&gt;
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; val = s.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;(); &lt;span class=&quot;cm&quot;&gt;// 먼저 읽고&lt;/span&gt;
    s.&lt;span class=&quot;fn&quot;&gt;pop&lt;/span&gt;();           &lt;span class=&quot;cm&quot;&gt;// 그 다음 제거&lt;/span&gt;
    &lt;span class=&quot;cm&quot;&gt;// int x = s.pop(); // ❌ 컴파일 에러 — pop()은 반환값 없음&lt;/span&gt;

    &lt;span class=&quot;kw&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;;
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;hr /&gt;

  &lt;!-- ══════════════════════════════════════════
       섹션 6 : 실전 문제 풀이
  ══════════════════════════════════════════ --&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;6&lt;/span&gt;실전 문제 풀이
  &lt;/div&gt;

  &lt;!-- 문제 9 --&gt;
  &lt;p&gt;&lt;strong&gt;문제 9 — 10진수를 2진수로 변환하기&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;
    정수 &lt;code&gt;decimal&lt;/code&gt;을 2진수 문자열로 반환한다.
    2로 나눈 나머지를 스택에 쌓은 뒤, 스택에서 순서대로 꺼내면 LIFO 특성에 의해
    자연스럽게 올바른 2진수 순서(MSB → LSB)가 완성된다.
  &lt;/p&gt;

  &lt;table&gt;
    &lt;thead&gt;&lt;tr&gt;&lt;th&gt;decimal&lt;/th&gt;&lt;th&gt;result&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;&quot;1010&quot;&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td&gt;27&lt;/td&gt;&lt;td&gt;&quot;11011&quot;&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td&gt;12345&lt;/td&gt;&lt;td&gt;&quot;11000000111001&quot;&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;dot-r&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-y&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-g&quot;&gt;&lt;/span&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ — 10진수 → 2진수 변환&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;stack&amp;gt;
&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;string&amp;gt;
&lt;span class=&quot;kw&quot;&gt;using namespace&lt;/span&gt; std;

string &lt;span class=&quot;fn&quot;&gt;solution&lt;/span&gt;(&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; decimal) {
    &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (decimal == &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;) &lt;span class=&quot;kw&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;str&quot;&gt;&quot;0&quot;&lt;/span&gt;;

    stack&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt; st;
    &lt;span class=&quot;kw&quot;&gt;while&lt;/span&gt; (decimal &amp;gt; &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;) {
        st.&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(decimal % &lt;span class=&quot;num&quot;&gt;2&lt;/span&gt;); &lt;span class=&quot;cm&quot;&gt;// 나머지를 스택에 쌓음&lt;/span&gt;
        decimal /= &lt;span class=&quot;num&quot;&gt;2&lt;/span&gt;;
    }

    string binary = &lt;span class=&quot;str&quot;&gt;&quot;&quot;&lt;/span&gt;;
    &lt;span class=&quot;kw&quot;&gt;while&lt;/span&gt; (!st.&lt;span class=&quot;fn&quot;&gt;empty&lt;/span&gt;()) {
        binary += to_string(st.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;()); &lt;span class=&quot;cm&quot;&gt;// LIFO → MSB부터 꺼내짐&lt;/span&gt;
        st.&lt;span class=&quot;fn&quot;&gt;pop&lt;/span&gt;();
    }
    &lt;span class=&quot;kw&quot;&gt;return&lt;/span&gt; binary;
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;!-- 문제 12 --&gt;
  &lt;p&gt;&lt;strong&gt;문제 12 — 주식 가격 (프로그래머스)&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;
    초 단위로 기록된 주식 가격 배열 &lt;code&gt;prices&lt;/code&gt;가 주어질 때,
    각 시점에서 가격이 떨어지지 않은 기간(초)을 반환한다.
    스택에 인덱스를 저장하고 현재 가격이 이전 가격보다 낮을 때 pop하여 기간을 계산한다.
  &lt;/p&gt;

  &lt;div class=&quot;note&quot;&gt;
    &lt;strong&gt;핵심 아이디어 — 단조 스택(Monotone Stack)&lt;/strong&gt;&lt;br /&gt;
    스택에 저장된 인덱스의 가격이 항상 현재 가격 이하를 유지하도록 관리한다.
    현재 가격이 더 낮으면 스택을 pop하면서 그 시점의 지속 기간을 &lt;code&gt;i - s.top()&lt;/code&gt;으로 계산한다.
    순회 후 스택에 남은 인덱스는 끝까지 가격이 떨어지지 않은 경우다.
  &lt;/div&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;dot-r&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-y&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-g&quot;&gt;&lt;/span&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ — 주식 가격&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;vector&amp;gt;
&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;stack&amp;gt;
&lt;span class=&quot;kw&quot;&gt;using namespace&lt;/span&gt; std;

vector&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt; &lt;span class=&quot;fn&quot;&gt;solution&lt;/span&gt;(vector&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt; prices) {
    vector&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt; answer(prices.&lt;span class=&quot;fn&quot;&gt;size&lt;/span&gt;());
    stack&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt; s;
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; n = prices.&lt;span class=&quot;fn&quot;&gt;size&lt;/span&gt;();

    &lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; i &amp;lt; n; i++) {
        &lt;span class=&quot;kw&quot;&gt;while&lt;/span&gt; (!s.&lt;span class=&quot;fn&quot;&gt;empty&lt;/span&gt;() &amp;amp;&amp;amp; prices[s.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;()] &amp;gt; prices[i]) {
            &lt;span class=&quot;cm&quot;&gt;// 가격이 떨어진 시점 → 기간 계산 후 pop&lt;/span&gt;
            answer[s.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;()] = i - s.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;();
            s.&lt;span class=&quot;fn&quot;&gt;pop&lt;/span&gt;();
        }
        s.&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(i);
    }
    &lt;span class=&quot;cm&quot;&gt;// 끝까지 가격이 떨어지지 않은 인덱스 처리&lt;/span&gt;
    &lt;span class=&quot;kw&quot;&gt;while&lt;/span&gt; (!s.&lt;span class=&quot;fn&quot;&gt;empty&lt;/span&gt;()) {
        answer[s.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;()] = n - s.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;() - &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;;
        s.&lt;span class=&quot;fn&quot;&gt;pop&lt;/span&gt;();
    }
    &lt;span class=&quot;kw&quot;&gt;return&lt;/span&gt; answer;
}
&lt;span class=&quot;cm&quot;&gt;// solution({1,2,3,2,3}) → {4,3,1,1,0}&lt;/span&gt;&lt;/pre&gt;
  &lt;/div&gt;

  &lt;!-- 문제 13 --&gt;
  &lt;p&gt;&lt;strong&gt;문제 13 — 크레인 인형 뽑기 게임 (프로그래머스)&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;
    N×N 격자 보드에서 크레인이 지정된 열 순서(&lt;code&gt;moves&lt;/code&gt;)로 인형을 뽑는다.
    뽑은 인형을 바구니 스택에 쌓을 때, 스택 top과 같은 인형이 오면 두 인형이 제거된다.
    최종적으로 사라진 인형의 총 개수를 반환한다.
  &lt;/p&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;dot-r&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-y&quot;&gt;&lt;/span&gt;&lt;span class=&quot;dot-g&quot;&gt;&lt;/span&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ — 크레인 인형 뽑기 게임&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;stack&amp;gt;
&lt;span class=&quot;kw&quot;&gt;#include&lt;/span&gt; &amp;lt;vector&amp;gt;
&lt;span class=&quot;kw&quot;&gt;using namespace&lt;/span&gt; std;

&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;fn&quot;&gt;solution&lt;/span&gt;(vector&amp;lt;vector&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt;&amp;gt; board, vector&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt; moves) {
    &lt;span class=&quot;cm&quot;&gt;// 각 열을 스택으로 모델링 (아래→위 순서로 push)&lt;/span&gt;
    stack&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt; lanes[board[&lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;].&lt;span class=&quot;fn&quot;&gt;size&lt;/span&gt;()];
    &lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = board.&lt;span class=&quot;fn&quot;&gt;size&lt;/span&gt;()-&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;; i &amp;gt;= &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; --i)
        &lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; j = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; j &amp;lt; board[&lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;].&lt;span class=&quot;fn&quot;&gt;size&lt;/span&gt;(); ++j)
            &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (board[i][j]) lanes[j].&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(board[i][j]);

    stack&amp;lt;&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;&amp;gt; bucket;
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; answer = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;;

    &lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; m : moves) {
        &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (lanes[m-&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;].&lt;span class=&quot;fn&quot;&gt;size&lt;/span&gt;()) {
            &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; doll = lanes[m-&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;].&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;();
            lanes[m-&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;].&lt;span class=&quot;fn&quot;&gt;pop&lt;/span&gt;();
            &lt;span class=&quot;cm&quot;&gt;// 바구니 top과 같으면 둘 다 제거 (+2점)&lt;/span&gt;
            &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (bucket.&lt;span class=&quot;fn&quot;&gt;size&lt;/span&gt;() &amp;amp;&amp;amp; bucket.&lt;span class=&quot;fn&quot;&gt;top&lt;/span&gt;() == doll) {
                bucket.&lt;span class=&quot;fn&quot;&gt;pop&lt;/span&gt;();
                answer += &lt;span class=&quot;num&quot;&gt;2&lt;/span&gt;;
            } &lt;span class=&quot;kw&quot;&gt;else&lt;/span&gt; {
                bucket.&lt;span class=&quot;fn&quot;&gt;push&lt;/span&gt;(doll);
            }
        }
    }
    &lt;span class=&quot;kw&quot;&gt;return&lt;/span&gt; answer;
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;hr /&gt;

  &lt;!-- ══════════════════════════════════════════
       섹션 7 : 오늘의 회고
  ══════════════════════════════════════════ --&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;7&lt;/span&gt;오늘의 회고
  &lt;/div&gt;

  &lt;table class=&quot;retrospect&quot;&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;새로 배운 것&lt;/td&gt;
        &lt;td&gt;
          스택의 LIFO 원리가 웹 브라우저 뒤로 가기, 함수 콜 스택 등 실생활과 직결되어 있다는 것을 코드로 직접 확인했다.
          ADT 개념을 통해 push/pop 내부에서 top 인덱스가 어떻게 움직이는지 구체적으로 이해했다.
          STL stack에서 &lt;code&gt;pop()&lt;/code&gt;이 반환값이 없으므로 반드시 &lt;code&gt;top()&lt;/code&gt;을 먼저 써야 한다는 점도 새롭게 정리됐다.
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;어려웠던 점&lt;/td&gt;
        &lt;td&gt;
          주식 가격 문제에서 단조 스택 패턴을 처음 접했을 때, 스택에 값이 아닌 인덱스를 저장한다는 발상이 직관적이지 않았다.
          크레인 인형 뽑기 역시 보드를 열 단위 스택으로 모델링하는 아이디어를 떠올리는 데 시간이 걸렸다.
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;기억할 핵심&lt;/td&gt;
        &lt;td&gt;
          &quot;가장 최근 원소&quot;가 키워드라면 스택을 의심하라. 값 대신 인덱스를 스택에 저장하면 구간 계산이 훨씬 쉬워진다.
          &lt;code&gt;top() → pop()&lt;/code&gt; 순서는 반드시 지킨다.
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;다음 목표&lt;/td&gt;
        &lt;td&gt;
          큐(Queue) 자료구조와 STL &lt;code&gt;&amp;lt;queue&amp;gt;&lt;/code&gt; 활용법을 학습하고, 스택과 큐를 함께 사용하는 문제 유형까지 연습한다.
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</description>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/104</guid>
      <comments>https://hanong8.tistory.com/104#entry104comment</comments>
      <pubDate>Thu, 14 May 2026 19:43:40 +0900</pubDate>
    </item>
    <item>
      <title>[C++] 코딩테스트에서의 시뮬레이션</title>
      <link>https://hanong8.tistory.com/103</link>
      <description>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
&lt;style&gt;
  @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap');

  :root {
    --bg: #1a1d23;
    --bg2: #20242e;
    --bg3: #252a36;
    --border: #2e3444;
    --text: #d4d8e2;
    --text-dim: #7a8394;
    --text-bright: #eef0f6;
    --accent: #5b9cf6;
    --accent2: #7c6df0;
    --accent-green: #3ecf8e;
    --accent-yellow: #f5c542;
    --accent-red: #e5534b;
    --code-bg: #0e1117;
    --num-bg: #2b3045;
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }

  body {
    background: var(--bg);
    color: var(--text);
    font-family: 'Noto Sans KR', sans-serif;
    font-size: 15px;
    line-height: 1.8;
    padding: 32px 20px 72px;
    max-width: 760px;
    margin: 0 auto;
  }

  .section-num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px; height: 26px;
    background: var(--num-bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    font-weight: 700;
    color: var(--accent);
    margin-right: 10px;
    flex-shrink: 0;
  }

  .section-title {
    display: flex;
    align-items: center;
    font-size: 18px;
    font-weight: 700;
    color: var(--text-bright);
    margin-bottom: 16px;
  }

  p {
    color: var(--text);
    line-height: 1.85;
    font-size: 14.5px;
    margin-bottom: 12px;
  }
  p:last-child { margin-bottom: 0; }

  strong { color: var(--text-bright); font-weight: 600; }

  code {
    font-family: 'JetBrains Mono', monospace;
    font-size: 12.5px;
    background: rgba(91,156,246,0.12);
    border: 1px solid rgba(91,156,246,0.18);
    border-radius: 4px;
    padding: 1px 6px;
    color: #8ab8ff;
  }

  hr {
    border: none;
    border-top: 1px solid var(--border);
    margin: 28px 0;
  }

  table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13.5px;
    margin: 14px 0;
  }
  th {
    background: var(--bg3);
    color: var(--text-dim);
    font-size: 12px;
    font-family: 'JetBrains Mono', monospace;
    font-weight: 600;
    letter-spacing: 0.3px;
    padding: 10px 14px;
    text-align: left;
    border-bottom: 1px solid var(--border);
    border-top: 1px solid var(--border);
  }
  td {
    padding: 10px 14px;
    border-bottom: 1px solid rgba(46,52,68,0.7);
    color: var(--text);
    vertical-align: middle;
    line-height: 1.65;
  }
  tr:last-child td { border-bottom: none; }
  tr:hover td { background: rgba(255,255,255,0.015); }

  .note {
    background: rgba(91,156,246,0.07);
    border-left: 3px solid var(--accent);
    border-radius: 0 8px 8px 0;
    padding: 12px 16px;
    font-size: 14px;
    color: #a8c8ff;
    margin: 14px 0;
    line-height: 1.75;
  }
  .warn-box {
    background: rgba(245,197,66,0.07);
    border-left: 3px solid var(--accent-yellow);
    border-radius: 0 8px 8px 0;
    padding: 12px 16px;
    font-size: 14px;
    color: #ead98e;
    margin: 14px 0;
    line-height: 1.75;
  }

  .step-list { display: flex; flex-direction: column; gap: 8px; margin: 14px 0; }
  .step-item { display: flex; gap: 12px; align-items: flex-start; }
  .step-badge {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 22px; height: 22px;
    background: var(--num-bg);
    border: 1px solid var(--border);
    border-radius: 50%;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px; font-weight: 700;
    color: var(--accent);
    flex-shrink: 0; margin-top: 2px;
  }
  .step-text { font-size: 14.5px; color: var(--text); line-height: 1.8; }

  .code-wrap {
    background: var(--code-bg);
    border: 1px solid #1e2430;
    border-radius: 10px;
    overflow: hidden;
    margin: 14px 0;
  }
  .code-header {
    display: flex; align-items: center; justify-content: space-between;
    background: #161b26;
    padding: 8px 14px;
    border-bottom: 1px solid #1e2430;
  }
  .code-dots { display: flex; gap: 5px; }
  .code-dot { width: 10px; height: 10px; border-radius: 50%; }
  .code-dot.r { background: #ff5f56; }
  .code-dot.y { background: #ffbd2e; }
  .code-dot.g { background: #27c93f; }
  .code-lang { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #505a70; }
  pre {
    padding: 16px 18px;
    overflow-x: auto;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12.5px;
    line-height: 1.75;
    color: #cdd5e0;
    white-space: pre;
  }
  .kw  { color: #e06c75; }
  .fn  { color: #c9a8ff; }
  .cm  { color: #5c6882; font-style: italic; }
  .tp  { color: #e5c07b; }
  .num { color: #79c0ff; }

  .recap-table th { color: var(--text-dim); }
  .recap-table td:first-child { font-weight: 600; color: var(--text-bright); white-space: nowrap; }

  .o      { font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#8ab8ff; }
  .o-good { font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#5ce8a8; }
  .o-bad  { font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#f08080; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;


&lt;!-- 1. 개요 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;1&lt;/span&gt;개요
  &lt;/div&gt;
  &lt;p&gt;
    시뮬레이션은 &lt;strong&gt;문제에서 제시한 규칙을 코드로 그대로 옮겨 구현&lt;/strong&gt;하는 유형이다.
    별도의 알고리즘 공식보다는 조건과 흐름을 정확히 파악하고 실수 없이 구현하는 능력이 핵심이다.
  &lt;/p&gt;
  &lt;p&gt;
    이번 챕터에서는 2차원 배열 기반의 &lt;strong&gt;좌표 연산, 대칭·회전 변환, 대각선 순회, 행렬 연산&lt;/strong&gt;을 다룬다.
    모두 코딩 테스트에서 반복적으로 출제되는 핵심 패턴이다.
  &lt;/p&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 2. 좌표 표현 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;2&lt;/span&gt;y, x 좌표 표현하기
  &lt;/div&gt;
  &lt;p&gt;
    코딩 테스트에서 2차원 평면은 &lt;strong&gt;2차원 배열&lt;/strong&gt;로 표현하는 것이 가장 직관적이다.
    이때 &lt;strong&gt;y좌표 → 행(row) 인덱스&lt;/strong&gt;, &lt;strong&gt;x좌표 → 열(col) 인덱스&lt;/strong&gt;로 대응시킨다.
  &lt;/p&gt;
  &lt;p&gt;
    예를 들어, 좌표 (3, 4)는 배열의 &lt;code&gt;arr[4][3]&lt;/code&gt;에 해당한다.
    수학 좌표계와 배열 인덱스의 y축 방향이 반대임을 항상 주의해야 한다.
  &lt;/p&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;수학 좌표&lt;/th&gt;&lt;th&gt;배열 표현&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td&gt;(x, y)&lt;/td&gt;&lt;td&gt;&lt;code&gt;arr[y][x]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;y = 행 인덱스, x = 열 인덱스&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td&gt;(3, 4)&lt;/td&gt;&lt;td&gt;&lt;code&gt;arr[4][3]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;4행 3열&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 3. dy, dx 오프셋 배열 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;3&lt;/span&gt;dy, dx 배열로 인접 좌표 이동하기
  &lt;/div&gt;
  &lt;p&gt;
    현재 좌표에서 인접한 좌표로 이동할 때 &lt;strong&gt;오프셋 배열 &lt;code&gt;dy&lt;/code&gt;, &lt;code&gt;dx&lt;/code&gt;&lt;/strong&gt;를 사용하면
    하드코딩 없이 반복문으로 깔끔하게 처리할 수 있다.
    실제 시뮬레이션 문제의 대부분이 이 방식을 활용한다.
  &lt;/p&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;div class=&quot;code-dot r&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot y&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot g&quot;&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ · dy, dx 오프셋 배열 (8방향)&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;cm&quot;&gt;// 8방향: 상좌, 상, 상우, 우, 하우, 하, 하좌, 좌&lt;/span&gt;
&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; dy[] = {-&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;, -&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;, -&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;,  &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;, &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;,  &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;,  &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;};
&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; dx[] = {-&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;,  &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;,  &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;,  &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;, -&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;, -&lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;};

&lt;span class=&quot;cm&quot;&gt;// 현재 위치 (y, x)에서 인접한 8개 좌표 순회&lt;/span&gt;
&lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;num&quot;&gt;8&lt;/span&gt;; i++) {
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; ny = y + dy[i];
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; nx = x + dx[i];
    &lt;span class=&quot;cm&quot;&gt;// 범위 체크 후 처리&lt;/span&gt;
    &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (ny &amp;gt;= &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt; &amp;amp;&amp;amp; ny &amp;lt; N &amp;amp;&amp;amp; nx &amp;gt;= &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt; &amp;amp;&amp;amp; nx &amp;lt; N) {
        &lt;span class=&quot;cm&quot;&gt;// 유효한 인접 좌표 처리&lt;/span&gt;
    }
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;div class=&quot;note&quot;&gt;
      4방향만 필요하다면 &lt;code&gt;dy = {-1, 1, 0, 0}&lt;/code&gt;, &lt;code&gt;dx = {0, 0, -1, 1}&lt;/code&gt;으로 줄일 수 있다.
    같은 인덱스끼리 짝을 이루는 구조가 핵심이다.
  &lt;/div&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 4. 대칭과 회전 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;4&lt;/span&gt;대칭과 회전 변환 공식
  &lt;/div&gt;
  &lt;p&gt;
    N×N 배열에서 좌표 변환 공식을 암기해 두면 시뮬레이션 문제에서 바로 적용할 수 있다.
  &lt;/p&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;변환&lt;/th&gt;&lt;th&gt;공식 (Y, X) →&lt;/th&gt;&lt;th&gt;변화하지 않는 축&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;좌우 대칭&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;(Y, (N-1) - X)&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;Y 고정, X만 변환&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;상하 대칭&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;((N-1) - Y, X)&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;X 고정, Y만 변환&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;90도 회전 (시계)&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;((N-1) - X, Y)&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;행·열 모두 변환&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;180도 회전&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;90도 회전 2회 적용&lt;/td&gt;
        &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;270도 회전&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;90도 회전 3회 적용&lt;/td&gt;
        &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;div class=&quot;warn-box&quot;&gt;
    ⚠️ 90도 회전 후 결과를 원본 배열에 바로 쓰면 덮어쓰기 충돌이 발생한다.
    반드시 &lt;strong&gt;별도의 임시 배열&lt;/strong&gt;에 결과를 저장한 뒤 원본에 복사해야 한다.
  &lt;/div&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 5. 대각선 행렬 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;5&lt;/span&gt;대각선 행렬 순회
  &lt;/div&gt;
  &lt;p&gt;
    같은 대각선 상에 있는 원소들을 판별하는 조건과 이동 방향을 정리한다.
  &lt;/p&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;종류&lt;/th&gt;&lt;th&gt;다음 원소 이동&lt;/th&gt;&lt;th&gt;같은 대각선 판별 조건&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;정방향 대각선 (↘)&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;(Y+1, X+1)&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;Y - A == X - B&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;역방향 대각선 (↙)&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;(Y+1, X-1)&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;Y - A == B - X&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;p&gt;
    정방향은 &lt;code&gt;Y - X&lt;/code&gt; 값이 같은 원소끼리 같은 대각선이고,
    역방향은 &lt;code&gt;Y + X&lt;/code&gt; 값이 같은 원소끼리 같은 대각선이다.
  &lt;/p&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 6. 행렬 연산 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;6&lt;/span&gt;행렬 연산
  &lt;/div&gt;

  &lt;p&gt;&lt;strong&gt;행렬 덧셈 / 뺄셈&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;
    두 행렬의 크기가 같을 때, &lt;strong&gt;같은 위치의 원소끼리 더하거나 빼는&lt;/strong&gt; 연산이다.
    결과 행렬의 크기는 입력 행렬과 동일하다.
  &lt;/p&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;div class=&quot;code-dot r&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot y&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot g&quot;&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ · 행렬 덧셈&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; i &amp;lt; N; i++)
    &lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; j = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; j &amp;lt; N; j++)
        C[i][j] = A[i][j] + B[i][j];&lt;/pre&gt;
  &lt;/div&gt;

  &lt;p&gt;&lt;strong&gt;행렬 곱셈&lt;/strong&gt;&lt;/p&gt;
  &lt;p&gt;
    A의 &lt;strong&gt;X행&lt;/strong&gt;과 B의 &lt;strong&gt;Y열&lt;/strong&gt;의 원소를 각각 곱한 후 합산하여 &lt;code&gt;C[X][Y]&lt;/code&gt;를 완성한다.
    연산이 가능하려면 &lt;strong&gt;A의 열 수 == B의 행 수&lt;/strong&gt;여야 한다.
    결과 행렬 C의 크기는 &lt;strong&gt;A의 행 수 × B의 열 수&lt;/strong&gt;다.
  &lt;/p&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;div class=&quot;code-dot r&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot y&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot g&quot;&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ · 행렬 곱셈&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;cm&quot;&gt;// A: N×M, B: M×K → C: N×K&lt;/span&gt;
&lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; i &amp;lt; N; i++)
    &lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; j = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; j &amp;lt; K; j++)
        &lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; k = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; k &amp;lt; M; k++)
            C[i][j] += A[i][k] * B[k][j];&lt;/pre&gt;
  &lt;/div&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;연산&lt;/th&gt;&lt;th&gt;전제 조건&lt;/th&gt;&lt;th&gt;결과 크기&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td&gt;&lt;strong&gt;덧셈 / 뺄셈&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;두 행렬 크기 동일&lt;/td&gt;&lt;td&gt;입력과 동일&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td&gt;&lt;strong&gt;곱셈&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;A의 열 수 == B의 행 수&lt;/td&gt;&lt;td&gt;A의 행 수 × B의 열 수&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 7. 회고 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;7&lt;/span&gt;오늘의 회고
  &lt;/div&gt;

  &lt;table class=&quot;recap-table&quot;&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;내용&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;새로 배운 것&lt;/td&gt;
        &lt;td&gt;좌표 변환 공식(좌우·상하 대칭, 90도 회전)을 표로 정리하니 패턴이 명확해졌다. 대각선 판별 조건도 &lt;code&gt;Y - X&lt;/code&gt;와 &lt;code&gt;Y + X&lt;/code&gt;로 단순화할 수 있다는 점이 유용했다.&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;어려웠던 점&lt;/td&gt;
        &lt;td&gt;90도 회전 시 임시 배열 없이 원본에 바로 쓰면 안 된다는 것을 직접 실수하면서 깨달았다. 변환 공식보다 구현 시 덮어쓰기 문제를 놓치기 쉽다.&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;기억할 핵심&lt;/td&gt;
        &lt;td&gt;dy·dx 오프셋 배열은 방향 이동 문제의 기본 패턴이므로 4방향·8방향 모두 손에 익혀둬야 한다. 행렬 곱셈은 &lt;code&gt;A열 == B행&lt;/code&gt; 조건과 3중 반복문 구조를 반드시 암기한다.&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;다음 목표&lt;/td&gt;
        &lt;td&gt;달팽이 수열(문제 64)과 캐릭터 좌표(문제 69)를 직접 구현해보고, 회전·대칭 변환이 포함된 추가 문제도 찾아서 풀어볼 예정이다.&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</description>
      <category>프로그래밍/코드카타</category>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/103</guid>
      <comments>https://hanong8.tistory.com/103#entry103comment</comments>
      <pubDate>Tue, 12 May 2026 23:09:40 +0900</pubDate>
    </item>
    <item>
      <title>[C++] 코딩테스트에서의 정렬</title>
      <link>https://hanong8.tistory.com/102</link>
      <description>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
&lt;style&gt;
  @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap');

  :root {
    --bg: #1a1d23;
    --bg2: #20242e;
    --bg3: #252a36;
    --border: #2e3444;
    --text: #d4d8e2;
    --text-dim: #7a8394;
    --text-bright: #eef0f6;
    --accent: #5b9cf6;
    --accent2: #7c6df0;
    --accent-green: #3ecf8e;
    --accent-yellow: #f5c542;
    --accent-red: #e5534b;
    --code-bg: #0e1117;
    --num-bg: #2b3045;
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }

  body {
    background: var(--bg);
    color: var(--text);
    font-family: 'Noto Sans KR', sans-serif;
    font-size: 15px;
    line-height: 1.8;
    padding: 32px 20px 72px;
    max-width: 760px;
    margin: 0 auto;
  }

  /* ── 최상단 헤더 ── */
  .post-header {
    margin-bottom: 36px;
    padding-bottom: 20px;
    border-bottom: 1px solid var(--border);
  }
  .post-category {
    font-size: 12px;
    color: var(--accent);
    font-family: 'JetBrains Mono', monospace;
    letter-spacing: 0.5px;
    margin-bottom: 10px;
  }
  .post-title {
    font-size: 26px;
    font-weight: 700;
    color: var(--text-bright);
    line-height: 1.3;
    margin-bottom: 8px;
  }
  .post-meta {
    font-size: 12.5px;
    color: var(--text-dim);
  }

  /* ── 섹션 번호 뱃지 ── */
  .section-num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    background: var(--num-bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    font-weight: 700;
    color: var(--accent);
    margin-right: 10px;
    vertical-align: middle;
    flex-shrink: 0;
  }

  /* ── 섹션 제목 ── */
  .section-title {
    display: flex;
    align-items: center;
    font-size: 18px;
    font-weight: 700;
    color: var(--text-bright);
    margin-bottom: 16px;
  }

  /* ── 본문 단락 ── */
  p {
    color: var(--text);
    line-height: 1.85;
    font-size: 14.5px;
    margin-bottom: 12px;
  }
  p:last-child { margin-bottom: 0; }

  strong { color: var(--text-bright); font-weight: 600; }

  /* ── 인라인 코드 ── */
  code {
    font-family: 'JetBrains Mono', monospace;
    font-size: 12.5px;
    background: rgba(91,156,246,0.12);
    border: 1px solid rgba(91,156,246,0.18);
    border-radius: 4px;
    padding: 1px 6px;
    color: #8ab8ff;
  }

  /* ── hr 구분선 ── */
  hr {
    border: none;
    border-top: 1px solid var(--border);
    margin: 28px 0;
  }

  /* ── 섹션 wrapper ── */
  .section { margin-bottom: 0; }

  /* ── 일반 표 ── */
  table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13.5px;
    margin: 14px 0;
  }
  th {
    background: var(--bg3);
    color: var(--text-dim);
    font-size: 12px;
    font-family: 'JetBrains Mono', monospace;
    font-weight: 600;
    letter-spacing: 0.3px;
    padding: 10px 14px;
    text-align: left;
    border-bottom: 1px solid var(--border);
    border-top: 1px solid var(--border);
  }
  td {
    padding: 10px 14px;
    border-bottom: 1px solid rgba(46,52,68,0.7);
    color: var(--text);
    vertical-align: middle;
    line-height: 1.65;
  }
  tr:last-child td { border-bottom: none; }
  tr:hover td { background: rgba(255,255,255,0.015); }

  /* ── 복잡도 뱃지 ── */
  .c-on  { font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; padding:2px 7px; border-radius:4px; background:rgba(62,207,142,0.15); color:#5ce8a8; white-space:nowrap; }
  .c-on2 { font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; padding:2px 7px; border-radius:4px; background:rgba(91,156,246,0.15); color:#8ab8ff; white-space:nowrap; }
  .c-nlogn { font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; padding:2px 7px; border-radius:4px; background:rgba(91,156,246,0.15); color:#8ab8ff; white-space:nowrap; }
  .c-n2  { font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; padding:2px 7px; border-radius:4px; background:rgba(229,83,75,0.13); color:#f08080; white-space:nowrap; }
  .c-nk  { font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; padding:2px 7px; border-radius:4px; background:rgba(62,207,142,0.15); color:#5ce8a8; white-space:nowrap; }
  .c-k   { font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; padding:2px 7px; border-radius:4px; background:rgba(124,109,240,0.15); color:#b09aff; white-space:nowrap; }
  .c-warn{ font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; padding:2px 7px; border-radius:4px; background:rgba(245,197,66,0.13); color:#f5d87a; white-space:nowrap; }
  .o { font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#8ab8ff; }
  .o-good { font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#5ce8a8; }
  .o-bad  { font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#f08080; }
  .o-mid  { font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#f5d87a; }

  /* ── 코드 블록 ── */
  .code-wrap {
    background: var(--code-bg);
    border: 1px solid #1e2430;
    border-radius: 10px;
    overflow: hidden;
    margin: 14px 0;
  }
  .code-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: #161b26;
    padding: 8px 14px;
    border-bottom: 1px solid #1e2430;
  }
  .code-dots { display: flex; gap: 5px; }
  .code-dot { width: 10px; height: 10px; border-radius: 50%; }
  .code-dot.r { background: #ff5f56; }
  .code-dot.y { background: #ffbd2e; }
  .code-dot.g { background: #27c93f; }
  .code-lang {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    color: #505a70;
    letter-spacing: 0.3px;
  }
  pre {
    padding: 16px 18px;
    overflow-x: auto;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12.5px;
    line-height: 1.75;
    color: #cdd5e0;
    white-space: pre;
  }
  .kw  { color: #e06c75; }
  .fn  { color: #c9a8ff; }
  .cm  { color: #5c6882; font-style: italic; }
  .tp  { color: #e5c07b; }
  .num { color: #79c0ff; }
  .str { color: #98c379; }

  /* ── 포인트 박스 ── */
  .note {
    background: rgba(91,156,246,0.07);
    border-left: 3px solid var(--accent);
    border-radius: 0 8px 8px 0;
    padding: 12px 16px;
    font-size: 14px;
    color: #a8c8ff;
    margin: 14px 0;
    line-height: 1.75;
  }
  .warn-box {
    background: rgba(245,197,66,0.07);
    border-left: 3px solid var(--accent-yellow);
    border-radius: 0 8px 8px 0;
    padding: 12px 16px;
    font-size: 14px;
    color: #ead98e;
    margin: 14px 0;
    line-height: 1.75;
  }
  .danger-box {
    background: rgba(229,83,75,0.07);
    border-left: 3px solid var(--accent-red);
    border-radius: 0 8px 8px 0;
    padding: 12px 16px;
    font-size: 14px;
    color: #f5a0a0;
    margin: 14px 0;
    line-height: 1.75;
  }

  /* ── 스텝 리스트 ── */
  .step-list { display: flex; flex-direction: column; gap: 8px; margin: 14px 0; }
  .step-item { display: flex; gap: 12px; align-items: flex-start; }
  .step-badge {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 22px; height: 22px;
    background: var(--num-bg);
    border: 1px solid var(--border);
    border-radius: 50%;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    font-weight: 700;
    color: var(--accent);
    flex-shrink: 0;
    margin-top: 2px;
  }
  .step-text { font-size: 14.5px; color: var(--text); line-height: 1.8; }

  /* ── 회고 표 ── */
  .recap-table th { color: var(--text-dim); }
  .recap-table td:first-child { font-weight: 600; color: var(--text-bright); white-space: nowrap; }

&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;




&lt;!-- 1. 개요 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;1&lt;/span&gt;개요
  &lt;/div&gt;

  &lt;p&gt;
    정렬은 &lt;strong&gt;사용자가 정의한 순서대로 데이터를 나열&lt;/strong&gt;하는 연산이다.
    오름차순·내림차순이 일반적이지만, 임의 조건도 가능하다.
    정렬이 필요한 이유는 크게 두 가지다.
    첫째, 데이터가 정렬되어 있으면 중앙값·최댓값을 훨씬 빠르게 구할 수 있다.
    둘째, &lt;strong&gt;이진 탐색처럼 정렬을 전제 조건으로 삼는 알고리즘&lt;/strong&gt;이 존재한다.
  &lt;/p&gt;
  &lt;p&gt;
    이번 챕터에서는 &lt;strong&gt;삽입 정렬, 병합 정렬, 계수 정렬, 힙 정렬&lt;/strong&gt; 네 가지를 다룬다.
    각 알고리즘의 동작 원리와 시간 복잡도를 비교하며 어떤 상황에 무엇을 써야 하는지
    판단 기준을 세우는 것이 목표다.
  &lt;/p&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 2. 삽입 정렬 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;2&lt;/span&gt;삽입 정렬 (Insertion Sort)
  &lt;/div&gt;

  &lt;p&gt;
    &lt;strong&gt;정렬되지 않은 원소를 이미 정렬된 부분 배열의 적절한 위치에 삽입&lt;/strong&gt;하며
    전체를 정렬하는 알고리즘이다. 첫 번째 원소는 이미 정렬된 것으로 간주하고
    두 번째 원소부터 처리한다.
  &lt;/p&gt;

  &lt;div class=&quot;step-list&quot;&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;1&lt;/span&gt;
      &lt;div class=&quot;step-text&quot;&gt;&lt;strong&gt;key 설정:&lt;/strong&gt; 현재 인덱스 &lt;code&gt;j&lt;/code&gt;의 값을 &lt;code&gt;key&lt;/code&gt;로 저장하고 &lt;code&gt;i = j - 1&lt;/code&gt;로 초기화한다.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;2&lt;/span&gt;
      &lt;div class=&quot;step-text&quot;&gt;&lt;strong&gt;밀어내기:&lt;/strong&gt; &lt;code&gt;A[i] &gt; key&lt;/code&gt;인 동안 &lt;code&gt;A[i+1] = A[i]&lt;/code&gt;로 원소를 한 칸씩 뒤로 밀고, &lt;code&gt;i--&lt;/code&gt;한다.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;3&lt;/span&gt;
      &lt;div class=&quot;step-text&quot;&gt;&lt;strong&gt;삽입:&lt;/strong&gt; 루프 종료 후 &lt;code&gt;A[i+1] = key&lt;/code&gt;로 알맞은 자리에 넣는다.&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;div class=&quot;code-dot r&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot y&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot g&quot;&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ · 삽입 정렬&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;; i &amp;lt; n; i++) {
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; key = arr[i];
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; j = i - &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;;

    &lt;span class=&quot;cm&quot;&gt;// key보다 큰 요소를 한 칸씩 뒤로 이동&lt;/span&gt;
    &lt;span class=&quot;kw&quot;&gt;while&lt;/span&gt; (j &amp;gt;= &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt; &amp;amp;&amp;amp; arr[j] &amp;gt; key) {
        arr[j + &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;] = arr[j];
        j--;
    }
    arr[j + &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;] = key;
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;p&gt;&lt;strong&gt;삽입 정렬의 시간 복잡도 — 최악 vs 최선&lt;/strong&gt;&lt;/p&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;경우&lt;/th&gt;&lt;th&gt;시간 복잡도&lt;/th&gt;&lt;th&gt;조건&lt;/th&gt;&lt;th&gt;이유&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;최악&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-bad&quot;&gt;O(N²)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;역순 정렬 데이터&lt;/td&gt;
        &lt;td&gt;매 단계 전체 원소를 밀어야 함. 비교 횟수 = N(N-1)/2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;최선&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-good&quot;&gt;O(N)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;이미 정렬된 데이터&lt;/td&gt;
        &lt;td&gt;각 원소마다 1번 비교 후 즉시 종료&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;div class=&quot;note&quot;&gt;
      &lt;strong&gt;거의 정렬된 데이터&lt;/strong&gt;에서 삽입 정렬은 O(N)에 가까워진다.
    이 특성 때문에 실무의 하이브리드 정렬(Timsort 등)에서 소규모 구간에 삽입 정렬을 활용한다.
  &lt;/div&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 3. 병합 정렬 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;3&lt;/span&gt;병합 정렬 (Merge Sort)
  &lt;/div&gt;

  &lt;p&gt;
    &lt;strong&gt;분할 정복(Divide &amp;amp; Conquer)&lt;/strong&gt; 기법을 활용한 정렬이다.
    배열을 계속 반으로 나눈 뒤 가장 작은 단위부터 정렬하며 합쳐 올라간다.
    크게 세 단계로 이루어진다.
  &lt;/p&gt;

  &lt;div class=&quot;step-list&quot;&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;1&lt;/span&gt;
      &lt;div class=&quot;step-text&quot;&gt;&lt;strong&gt;분할 (Divide):&lt;/strong&gt; 중간 지점 &lt;code&gt;mid = left + (right - left) / 2&lt;/code&gt; 기준으로 배열을 두 하위 배열로 나눈다.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;2&lt;/span&gt;
      &lt;div class=&quot;step-text&quot;&gt;&lt;strong&gt;정복 (Conquer):&lt;/strong&gt; 각 하위 배열을 재귀적으로 &lt;code&gt;mergeSort()&lt;/code&gt; 호출해 정렬한다. 원소가 1개가 될 때까지 반복한다.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;3&lt;/span&gt;
      &lt;div class=&quot;step-text&quot;&gt;&lt;strong&gt;결합 (Combine):&lt;/strong&gt; 두 정렬된 배열의 맨 앞 원소를 포인터로 비교해 작은 값을 먼저 임시 배열에 추가한다. 모두 소진되면 임시 배열을 원본에 복사한다.&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;div class=&quot;code-dot r&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot y&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot g&quot;&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ · 병합 정렬&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;kw&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fn&quot;&gt;mergeSort&lt;/span&gt;(&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; arr[], &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; left, &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; right) {
    &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (left &amp;lt; right) {
        &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; mid = left + (right - left) / &lt;span class=&quot;num&quot;&gt;2&lt;/span&gt;;  &lt;span class=&quot;cm&quot;&gt;// (left+right)/2는 오버플로우 위험&lt;/span&gt;
        &lt;span class=&quot;fn&quot;&gt;mergeSort&lt;/span&gt;(arr, left, mid);
        &lt;span class=&quot;fn&quot;&gt;mergeSort&lt;/span&gt;(arr, mid + &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;, right);
        &lt;span class=&quot;fn&quot;&gt;merge&lt;/span&gt;(arr, left, mid, right);
    }
}

&lt;span class=&quot;cm&quot;&gt;// merge() 핵심 — 두 포인터 비교&lt;/span&gt;
&lt;span class=&quot;kw&quot;&gt;while&lt;/span&gt; (i &amp;lt;= mid &amp;amp;&amp;amp; j &amp;lt;= right) {
    &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (arr[i] &amp;lt;= arr[j]) temp[k++] = arr[i++];
    &lt;span class=&quot;kw&quot;&gt;else&lt;/span&gt;                  temp[k++] = arr[j++];
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;p&gt;
    분할 트리의 높이는 &lt;code&gt;log N&lt;/code&gt;층이고, 각 층에서 모든 원소를 한 번씩 병합하므로
    한 층당 &lt;code&gt;O(N)&lt;/code&gt;이 걸린다. 따라서 전체 시간 복잡도는 &lt;span class=&quot;o&quot;&gt;O(N log N)&lt;/span&gt;이며
    &lt;strong&gt;최선·평균·최악 모두 동일&lt;/strong&gt;하다.
    단, 병합 과정에서 임시 배열이 필요해 &lt;strong&gt;추가 메모리 O(N)&lt;/strong&gt;이 사용된다.
  &lt;/p&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 4. 계수 정렬 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;4&lt;/span&gt;계수 정렬 (Counting Sort)
  &lt;/div&gt;

  &lt;p&gt;
    &lt;strong&gt;데이터값 자체를 인덱스로 사용&lt;/strong&gt;하는 방식이다.
    각 값의 등장 횟수를 &lt;code&gt;count[]&lt;/code&gt; 배열에 기록한 뒤,
    인덱스 순서대로 횟수만큼 꺼내면 정렬 결과가 된다.
    비교 연산을 전혀 하지 않기 때문에 조건만 맞으면 매우 빠르다.
  &lt;/p&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;div class=&quot;code-dot r&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot y&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot g&quot;&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ · 계수 정렬&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;cm&quot;&gt;// 1. 최댓값 K 탐색&lt;/span&gt;
&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; maxVal = *max_element(arr, arr + n);

&lt;span class=&quot;cm&quot;&gt;// 2. count 배열 생성 및 빈도수 기록 — O(K) + O(N)&lt;/span&gt;
&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;* count = &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt;[maxVal + &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;]{};
&lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; i &amp;lt; n; ++i)
    count[arr[i]]++;

&lt;span class=&quot;cm&quot;&gt;// 3. 인덱스 순으로 빈도수만큼 결과 생성 — O(N)&lt;/span&gt;
&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; idx = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;;
&lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; i &amp;lt;= maxVal; ++i)
    &lt;span class=&quot;kw&quot;&gt;while&lt;/span&gt; (count[i]--) arr[idx++] = i;

&lt;span class=&quot;kw&quot;&gt;delete&lt;/span&gt;[] count;
&lt;span class=&quot;cm&quot;&gt;// 입출력: {4,2,2,8,3,3,1} → {1,2,2,3,3,4,8}&lt;/span&gt;&lt;/pre&gt;
  &lt;/div&gt;

  &lt;p&gt;
    N은 입력 배열 크기, K는 최댓값이라 할 때 시간 복잡도는 &lt;span class=&quot;o-good&quot;&gt;O(N + K)&lt;/span&gt;다.
    K가 충분히 작으면 비교 기반 정렬보다 훨씬 빠르다.
    단, 아래 두 가지 경우에는 사용할 수 없다.
  &lt;/p&gt;

  &lt;div class=&quot;danger-box&quot;&gt;
    ⚠️ &lt;strong&gt;계수 정렬을 쓰면 안 되는 경우&lt;/strong&gt;&lt;br&gt;
    ① &lt;strong&gt;음수 값이 포함된 경우&lt;/strong&gt; — 배열의 인덱스로 음수를 쓸 수 없다.&lt;br&gt;
    ② &lt;strong&gt;값의 범위(K)가 매우 큰 경우&lt;/strong&gt; — 데이터가 500과 10억이라면 10억 크기의 배열이 필요해 메모리가 폭발한다.
  &lt;/div&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 5. 힙 정렬 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;5&lt;/span&gt;힙 정렬 (Heap Sort)
  &lt;/div&gt;

  &lt;p&gt;
    &lt;strong&gt;힙(Heap)&lt;/strong&gt;이라는 자료구조를 활용한 정렬이다.
    힙은 조건을 만족하는 완전 이진 트리로,
    &lt;strong&gt;최대 힙(Max-Heap)&lt;/strong&gt;은 모든 부모 노드의 값이 자식 노드 이상이고,
    &lt;strong&gt;최소 힙(Min-Heap)&lt;/strong&gt;은 그 반대다.
    힙을 배열로 표현할 때 인덱스 규칙(0-based)은 다음과 같다.
  &lt;/p&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;관계&lt;/th&gt;&lt;th&gt;인덱스 수식&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td&gt;왼쪽 자식&lt;/td&gt;&lt;td&gt;&lt;code&gt;2 * i + 1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td&gt;오른쪽 자식&lt;/td&gt;&lt;td&gt;&lt;code&gt;2 * i + 2&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td&gt;부모&lt;/td&gt;&lt;td&gt;&lt;code&gt;(i - 1) / 2&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;p&gt;&lt;strong&gt;힙 정렬 동작 순서&lt;/strong&gt;&lt;/p&gt;

  &lt;div class=&quot;step-list&quot;&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;1&lt;/span&gt;
      &lt;div class=&quot;step-text&quot;&gt;
        &lt;strong&gt;최대 힙 구축 — build_max_heap():&lt;/strong&gt;
        인덱스 &lt;code&gt;n/2 - 1&lt;/code&gt;부터 0까지 역순으로 &lt;code&gt;heapify()&lt;/code&gt;를 호출한다.
        리프 노드는 자식이 없어 heapify가 불필요하므로 절반부터 시작한다.
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;2&lt;/span&gt;
      &lt;div class=&quot;step-text&quot;&gt;
        &lt;strong&gt;루트 추출 &amp;amp; 교환:&lt;/strong&gt;
        루트 &lt;code&gt;arr[0]&lt;/code&gt;(최댓값)과 배열 마지막 원소를 교환해 최댓값을 배열 뒤에 정착시킨다.
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;span class=&quot;step-badge&quot;&gt;3&lt;/span&gt;
      &lt;div class=&quot;step-text&quot;&gt;
        &lt;strong&gt;힙 크기 축소 &amp;amp; 반복:&lt;/strong&gt;
        힙 범위를 1 줄이고, 루트에 대해 다시 &lt;code&gt;heapify()&lt;/code&gt;를 수행한다.
        heap_size = 1이 될 때까지 반복하면 오름차순 정렬이 완료된다.
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;code-wrap&quot;&gt;
    &lt;div class=&quot;code-header&quot;&gt;
      &lt;div class=&quot;code-dots&quot;&gt;&lt;div class=&quot;code-dot r&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot y&quot;&gt;&lt;/div&gt;&lt;div class=&quot;code-dot g&quot;&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;span class=&quot;code-lang&quot;&gt;C++ · 힙 정렬&lt;/span&gt;
    &lt;/div&gt;
    &lt;pre&gt;&lt;span class=&quot;kw&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fn&quot;&gt;heapify&lt;/span&gt;(&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; arr[], &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; n, &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i) {
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; largest = i;
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; left  = &lt;span class=&quot;num&quot;&gt;2&lt;/span&gt; * i + &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;;
    &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; right = &lt;span class=&quot;num&quot;&gt;2&lt;/span&gt; * i + &lt;span class=&quot;num&quot;&gt;2&lt;/span&gt;;

    &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (left  &amp;lt; n &amp;amp;&amp;amp; arr[left]  &amp;gt; arr[largest]) largest = left;
    &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (right &amp;lt; n &amp;amp;&amp;amp; arr[right] &amp;gt; arr[largest]) largest = right;

    &lt;span class=&quot;kw&quot;&gt;if&lt;/span&gt; (largest != i) {
        swap(arr[i], arr[largest]);
        &lt;span class=&quot;fn&quot;&gt;heapify&lt;/span&gt;(arr, n, largest);  &lt;span class=&quot;cm&quot;&gt;// 재귀 — 교환된 위치 재정비&lt;/span&gt;
    }
}

&lt;span class=&quot;kw&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fn&quot;&gt;heapSort&lt;/span&gt;(&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; arr[], &lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; n) {
    &lt;span class=&quot;cm&quot;&gt;// 1. Build Max Heap&lt;/span&gt;
    &lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = n / &lt;span class=&quot;num&quot;&gt;2&lt;/span&gt; - &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;; i &amp;gt;= &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; i--)
        &lt;span class=&quot;fn&quot;&gt;heapify&lt;/span&gt;(arr, n, i);

    &lt;span class=&quot;cm&quot;&gt;// 2. 최댓값을 하나씩 배열 뒤로 정착&lt;/span&gt;
    &lt;span class=&quot;kw&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;tp&quot;&gt;int&lt;/span&gt; i = n - &lt;span class=&quot;num&quot;&gt;1&lt;/span&gt;; i &amp;gt;= &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;; i--) {
        swap(arr[&lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;], arr[i]);       &lt;span class=&quot;cm&quot;&gt;// 루트 ↔ 마지막 원소&lt;/span&gt;
        &lt;span class=&quot;fn&quot;&gt;heapify&lt;/span&gt;(arr, i, &lt;span class=&quot;num&quot;&gt;0&lt;/span&gt;);        &lt;span class=&quot;cm&quot;&gt;// 줄어든 범위에서 재정비&lt;/span&gt;
    }
}
&lt;span class=&quot;cm&quot;&gt;// {4,10,3,5,1} → {1,3,4,5,10}&lt;/span&gt;&lt;/pre&gt;
  &lt;/div&gt;

  &lt;p&gt;
    힙의 높이는 &lt;code&gt;log N&lt;/code&gt;이고, N번의 추출마다 &lt;code&gt;heapify()&lt;/code&gt;가 &lt;code&gt;O(log N)&lt;/code&gt;이므로
    전체 시간 복잡도는 &lt;span class=&quot;o&quot;&gt;O(N log N)&lt;/span&gt;이다.
    &lt;strong&gt;추가 메모리가 O(1)&lt;/strong&gt;이며 최선·평균·최악 모두 동일하다는 것이 장점이다.
  &lt;/p&gt;

  &lt;div class=&quot;note&quot;&gt;
      힙 자료구조는 정렬뿐 아니라 &lt;strong&gt;삽입/삭제 연산도 O(log N)&lt;/strong&gt;에 수행할 수 있어
    우선순위 큐(Priority Queue) 구현에도 폭넓게 사용된다.
  &lt;/div&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 6. 한눈에 보는 정리 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;6&lt;/span&gt;한눈에 보는 정리
  &lt;/div&gt;

  &lt;table&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;알고리즘&lt;/th&gt;&lt;th&gt;최선&lt;/th&gt;&lt;th&gt;평균&lt;/th&gt;&lt;th&gt;최악&lt;/th&gt;&lt;th&gt;추가 메모리&lt;/th&gt;&lt;th&gt;안정 정렬&lt;/th&gt;&lt;th&gt;특이사항&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;삽입 정렬&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-good&quot;&gt;O(N)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-bad&quot;&gt;O(N²)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-bad&quot;&gt;O(N²)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;O(1)&lt;/td&gt;
        &lt;td&gt;✓&lt;/td&gt;
        &lt;td&gt;거의 정렬된 경우 강력&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;병합 정렬&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o&quot;&gt;O(NlogN)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o&quot;&gt;O(NlogN)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o&quot;&gt;O(NlogN)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-mid&quot;&gt;O(N)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;✓&lt;/td&gt;
        &lt;td&gt;성능이 일정, 메모리 필요&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;계수 정렬&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-good&quot;&gt;O(N+K)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-good&quot;&gt;O(N+K)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-good&quot;&gt;O(N+K)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o-mid&quot;&gt;O(K)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;✓&lt;/td&gt;
        &lt;td&gt;음수·큰 K 사용 불가&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;힙 정렬&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o&quot;&gt;O(NlogN)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o&quot;&gt;O(NlogN)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;o&quot;&gt;O(NlogN)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;O(1)&lt;/td&gt;
        &lt;td&gt;✗&lt;/td&gt;
        &lt;td&gt;제자리 정렬, 성능 일정&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;hr&gt;

&lt;!-- 7. 회고 --&gt;
&lt;div class=&quot;section&quot;&gt;
  &lt;div class=&quot;section-title&quot;&gt;
    &lt;span class=&quot;section-num&quot;&gt;7&lt;/span&gt;오늘의 회고
  &lt;/div&gt;

  &lt;table class=&quot;recap-table&quot;&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;내용&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;새로 배운 것&lt;/td&gt;
        &lt;td&gt;삽입·병합·계수·힙 정렬의 동작 원리와 시간 복잡도 차이. 힙이 단순한 정렬 도구가 아니라 삽입/삭제도 O(log N)에 처리하는 자료구조라는 점이 인상적이었다.&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;어려웠던 점&lt;/td&gt;
        &lt;td&gt;&lt;code&gt;build_max_heap()&lt;/code&gt;에서 왜 &lt;code&gt;n/2 - 1&lt;/code&gt;부터 시작하는지 처음엔 직관적으로 이해가 안 됐다. 인덱스 규칙(&lt;code&gt;left = 2i+1&lt;/code&gt;)을 직접 그려보니, &lt;code&gt;n/2&lt;/code&gt; 이상 인덱스는 모두 리프 노드임을 확인할 수 있었다.&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;기억할 핵심&lt;/td&gt;
        &lt;td&gt;계수 정렬은 K가 작고 음수가 없을 때만 사용. 범용 상황에서는 병합·힙 정렬이 안전한 선택이며, 이미 거의 정렬된 데이터에는 삽입 정렬이 의외로 강하다.&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;다음 목표&lt;/td&gt;
        &lt;td&gt;계수 정렬 응용 문제(문자열 정렬), 커스텀 비교 함수를 이용한 정렬 문제 직접 풀어보기. 힙을 활용한 우선순위 큐 패턴도 별도로 정리할 예정이다.&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</description>
      <category>프로그래밍/코드카타</category>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/102</guid>
      <comments>https://hanong8.tistory.com/102#entry102comment</comments>
      <pubDate>Mon, 11 May 2026 21:15:00 +0900</pubDate>
    </item>
    <item>
      <title>[C++] 코딩테스트에서의 배열</title>
      <link>https://hanong8.tistory.com/101</link>
      <description>&lt;style&gt;
  .til-wrap {
    max-width: 720px;
    margin: 0 auto;
    font-family: 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif;
    color: #e5e7eb;
    line-height: 1.8;
    padding: 0 0 3rem;
  }
  .til-divider {
    border: none;
    border-top: 1px solid rgba(255,255,255,0.1);
    margin: 1.8rem 0;
  }
  .til-section-hd {
    font-size: 18px;
    font-weight: 700;
    color: #f9fafb;
    margin-bottom: 1rem;
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .til-num {
    width: 26px; height: 26px;
    border-radius: 50%;
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.15);
    font-size: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #9ca3af;
    font-weight: 700;
    flex-shrink: 0;
  }
  .til-p {
    font-size: 15px;
    line-height: 1.85;
    color: #d1d5db;
    margin-bottom: .9rem;
  }
  .til-p strong { color: #f9fafb; }
  code {
    font-family: 'JetBrains Mono', 'D2Coding', 'Fira Code', monospace;
    font-size: 13px;
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.12);
    border-radius: 4px;
    padding: 1px 6px;
    color: #93c5fd;
  }
  .til-card {
    background: rgba(255,255,255,0.05);
    border-radius: 10px;
    padding: 1rem 1.2rem;
    margin: .8rem 0;
    border: 1px solid rgba(255,255,255,0.08);
  }
  .til-card p { margin: 0; font-size: 14px; color: #d1d5db; line-height: 1.75; }
  .til-card p + p { margin-top: .5rem; }
  .til-card p strong { color: #f9fafb; }
  .til-hl {
    border-left: 3px solid #60a5fa;
    padding: .75rem 1.1rem;
    background: rgba(59,130,246,0.1);
    border-radius: 0 8px 8px 0;
    margin: .9rem 0;
  }
  .til-hl p { margin: 0; font-size: 14px; color: #bfdbfe; line-height: 1.75; }
  .til-hl p strong { color: #93c5fd; }
  .til-warn {
    background: rgba(251,191,36,0.08);
    border: 1px solid rgba(251,191,36,0.25);
    border-radius: 8px;
    padding: .75rem 1.1rem;
    margin: .8rem 0;
  }
  .til-warn p { margin: 0; font-size: 13px; color: #fde68a; line-height: 1.7; }
  .til-warn p strong { color: #fcd34d; }
  .til-danger {
    background: rgba(239,68,68,0.08);
    border: 1px solid rgba(239,68,68,0.25);
    border-radius: 8px;
    padding: .75rem 1.1rem;
    margin: .8rem 0;
  }
  .til-danger p { margin: 0; font-size: 13px; color: #fca5a5; line-height: 1.7; }
  .til-danger p strong { color: #f87171; }
  .two-col {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
    margin: .9rem 0;
  }
  .vs-card {
    background: rgba(255,255,255,0.04);
    border: 1px solid rgba(255,255,255,0.1);
    border-radius: 10px;
    padding: 1rem 1.1rem;
  }
  .vc-hd {
    font-size: 12px;
    font-weight: 700;
    font-family: 'JetBrains Mono', monospace;
    margin-bottom: 7px;
    letter-spacing: .03em;
  }
  .vc-blue  { color: #93c5fd; }
  .vc-green { color: #6ee7b7; }
  .vc-bad   { color: #fca5a5; }
  .vs-card .big { font-size: 17px; font-weight: 700; color: #f9fafb; margin-bottom: 5px; }
  .vs-card p { margin: 0; font-size: 13px; line-height: 1.6; color: #9ca3af; }
  .vs-card p strong { color: #f9fafb; }
  .summary-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
    margin: .9rem 0;
  }
  .sum-box {
    background: rgba(255,255,255,0.04);
    border-radius: 8px;
    padding: .8rem 1rem;
    border: 1px solid rgba(255,255,255,0.08);
  }
  .sum-lbl {
    font-size: 11px;
    font-weight: 700;
    color: #6b7280;
    letter-spacing: .06em;
    margin-bottom: 5px;
    text-transform: uppercase;
  }
  .sum-box p { margin: 0; font-size: 13px; line-height: 1.6; color: #d1d5db; }
  .sum-box p strong { color: #f9fafb; }
  .chart-title {
    font-size: 12px;
    font-weight: 700;
    color: #6b7280;
    margin-bottom: 6px;
    letter-spacing: .04em;
    text-transform: uppercase;
  }
  .chart-wrap { position: relative; width: 100%; margin: .9rem 0 .4rem; }
  .legend-row {
    display: flex;
    gap: 16px;
    font-size: 12px;
    color: #6b7280;
    margin-bottom: .8rem;
    flex-wrap: wrap;
  }
  .legend-item { display: flex; align-items: center; gap: 5px; }
  .legend-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
  .recap-table { width: 100%; border-collapse: collapse; font-size: 14px; margin: .9rem 0; }
  .recap-table th {
    background: rgba(255,255,255,0.07);
    padding: 10px 14px;
    text-align: left;
    font-weight: 700;
    color: #f9fafb;
    border: 1px solid rgba(255,255,255,0.1);
    font-size: 13px;
  }
  .recap-table td {
    padding: 9px 14px;
    border: 1px solid rgba(255,255,255,0.08);
    color: #d1d5db;
    vertical-align: top;
  }
  .recap-table tr:nth-child(even) td { background: rgba(255,255,255,0.03); }
  .tag-good {
    display: inline-block;
    background: rgba(16,185,129,0.15);
    color: #6ee7b7;
    font-size: 11px;
    font-weight: 700;
    padding: 2px 8px;
    border-radius: 12px;
  }
  .tag-bad {
    display: inline-block;
    background: rgba(239,68,68,0.15);
    color: #fca5a5;
    font-size: 11px;
    font-weight: 700;
    padding: 2px 8px;
    border-radius: 12px;
  }
  .code-block {
    background: rgba(0,0,0,0.3);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 10px;
    padding: 1rem 1.2rem;
    margin: .8rem 0;
    overflow-x: auto;
  }
  .code-block pre {
    margin: 0;
    font-family: 'JetBrains Mono', 'D2Coding', 'Fira Code', monospace;
    font-size: 13px;
    line-height: 1.75;
    color: #d1fae5;
    white-space: pre;
  }
  .ck  { color: #93c5fd; }
  .cf  { color: #86efac; }
  .cn  { color: #fbbf24; }
  .cc  { color: #6b7280; font-style: italic; }
  .cs  { color: #fca5a5; }
  .step-list { margin: .9rem 0; }
  .step-item { display: flex; gap: 12px; align-items: flex-start; margin-bottom: .75rem; }
  .step-badge {
    width: 22px; height: 22px;
    border-radius: 50%;
    background: rgba(96,165,250,0.2);
    border: 1px solid rgba(96,165,250,0.4);
    color: #93c5fd;
    font-size: 11px;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    margin-top: 3px;
    font-family: 'JetBrains Mono', monospace;
  }
  .step-text { font-size: 14px; color: #d1d5db; line-height: 1.7; }
  .step-text strong { color: #f9fafb; }

  /* 메모리 시각화 */
  .mem-vis {
    display: flex;
    gap: 0;
    margin: 1rem 0;
    overflow-x: auto;
  }
  .mem-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 64px;
  }
  .mem-idx {
    font-size: 11px;
    font-family: 'JetBrains Mono', monospace;
    color: #6b7280;
    margin-bottom: 4px;
  }
  .mem-val {
    width: 56px; height: 40px;
    background: rgba(96,165,250,0.12);
    border: 1px solid rgba(96,165,250,0.3);
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'JetBrains Mono', monospace;
    font-size: 14px;
    font-weight: 700;
    color: #bfdbfe;
    border-right: none;
  }
  .mem-cell:last-child .mem-val { border-right: 1px solid rgba(96,165,250,0.3); }
  .mem-addr {
    font-size: 10px;
    font-family: 'JetBrains Mono', monospace;
    color: #4b5563;
    margin-top: 4px;
  }

  /* 2D 배열 시각화 */
  .arr2d-wrap { margin: 1rem 0; }
  .arr2d-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
  .arr2d-lbl { font-size: 12px; font-family: 'JetBrains Mono', monospace; color: #6b7280; width: 40px; }
  .arr2d-cell {
    width: 48px; height: 36px;
    background: rgba(167,139,250,0.12);
    border: 1px solid rgba(167,139,250,0.3);
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px;
    font-weight: 700;
    color: #ddd6fe;
    border-right: none;
  }
  .arr2d-cell:last-child { border-right: 1px solid rgba(167,139,250,0.3); }

  /* 삽입 시각화 */
  .insert-vis {
    display: flex;
    align-items: center;
    gap: 4px;
    margin: .8rem 0;
    overflow-x: auto;
    padding: 4px 0;
  }
  .iv-cell {
    width: 44px; height: 36px;
    border-radius: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px;
    font-weight: 700;
  }
  .iv-exist { background: rgba(96,165,250,0.15); border: 1px solid rgba(96,165,250,0.3); color: #bfdbfe; }
  .iv-new   { background: rgba(52,211,153,0.2);  border: 1px solid rgba(52,211,153,0.4); color: #6ee7b7; }
  .iv-empty { background: rgba(255,255,255,0.03); border: 1px dashed rgba(255,255,255,0.1); color: #374151; }
  .iv-arrow { color: #4b5563; font-size: 16px; padding: 0 2px; }
  .iv-label { font-size: 11px; color: #6b7280; margin-left: 6px; white-space: nowrap; }
&lt;/style&gt;

&lt;div class=&quot;til-wrap&quot;&gt;

  &lt;!-- ── 1. 핵심 메시지 ── --&gt;
  &lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;1&lt;/span&gt;개요&lt;/div&gt;
  &lt;p class=&quot;til-p&quot;&gt;배열은 &lt;strong&gt;동일한 타입의 원소들을 연속된 메모리 공간에 저장&lt;/strong&gt;하는 가장 기본적인 자료구조다. 인덱스로 O(1) 접근이 가능하지만, 삽입 위치에 따라 시간 복잡도가 크게 달라진다. 맨 뒤 삽입은 O(1)이지만 맨 앞 삽입은 O(N)이 된다. 이 차이를 이해하면 어떤 자료구조를 선택해야 할지 판단하는 기준이 생긴다.&lt;/p&gt;

  &lt;hr class=&quot;til-divider&quot;&gt;

  &lt;!-- ── 2. 배열의 세 가지 특징 ── --&gt;
  &lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;2&lt;/span&gt;배열의 세 가지 특징&lt;/div&gt;

  &lt;div class=&quot;step-list&quot;&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;div class=&quot;step-badge&quot;&gt;1&lt;/div&gt;
      &lt;div class=&quot;step-text&quot;&gt;&lt;strong&gt;동질성:&lt;/strong&gt; 배열의 모든 원소는 동일한 타입이어야 한다. 정수형 배열이면 모든 원소가 &lt;code&gt;int&lt;/code&gt;, 문자형 배열이면 모든 원소가 &lt;code&gt;char&lt;/code&gt;다.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;div class=&quot;step-badge&quot;&gt;2&lt;/div&gt;
      &lt;div class=&quot;step-text&quot;&gt;&lt;strong&gt;연속된 메모리 할당:&lt;/strong&gt; 원소들이 메모리상에 연속적으로 배치된다. 단편화 문제가 적고, CPU 캐시를 효율적으로 활용할 수 있어 접근 속도가 빠르다.&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;step-item&quot;&gt;
      &lt;div class=&quot;step-badge&quot;&gt;3&lt;/div&gt;
      &lt;div class=&quot;step-text&quot;&gt;&lt;strong&gt;인덱스 기반 접근:&lt;/strong&gt; 첫 번째 원소는 인덱스 0. &lt;code&gt;arr[i]&lt;/code&gt;처럼 어떤 위치든 O(1) 상수 시간에 직접 접근할 수 있다.&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- 메모리 시각화 --&gt;
  &lt;p class=&quot;til-p&quot; style=&quot;font-size:14px;margin-bottom:.4rem&quot;&gt;&lt;strong style=&quot;color:#f9fafb&quot;&gt;int arr[5] = {10, 20, 30, 40, 50} — 메모리 배치&lt;/strong&gt;&lt;/p&gt;
  &lt;div class=&quot;mem-vis&quot;&gt;
    &lt;div class=&quot;mem-cell&quot;&gt;&lt;div class=&quot;mem-idx&quot;&gt;arr[0]&lt;/div&gt;&lt;div class=&quot;mem-val&quot;&gt;10&lt;/div&gt;&lt;div class=&quot;mem-addr&quot;&gt;0x...b0&lt;/div&gt;&lt;/div&gt;
    &lt;div class=&quot;mem-cell&quot;&gt;&lt;div class=&quot;mem-idx&quot;&gt;arr[1]&lt;/div&gt;&lt;div class=&quot;mem-val&quot;&gt;20&lt;/div&gt;&lt;div class=&quot;mem-addr&quot;&gt;0x...b4&lt;/div&gt;&lt;/div&gt;
    &lt;div class=&quot;mem-cell&quot;&gt;&lt;div class=&quot;mem-idx&quot;&gt;arr[2]&lt;/div&gt;&lt;div class=&quot;mem-val&quot;&gt;30&lt;/div&gt;&lt;div class=&quot;mem-addr&quot;&gt;0x...b8&lt;/div&gt;&lt;/div&gt;
    &lt;div class=&quot;mem-cell&quot;&gt;&lt;div class=&quot;mem-idx&quot;&gt;arr[3]&lt;/div&gt;&lt;div class=&quot;mem-val&quot;&gt;40&lt;/div&gt;&lt;div class=&quot;mem-addr&quot;&gt;0x...bc&lt;/div&gt;&lt;/div&gt;
    &lt;div class=&quot;mem-cell&quot;&gt;&lt;div class=&quot;mem-idx&quot;&gt;arr[4]&lt;/div&gt;&lt;div class=&quot;mem-val&quot;&gt;50&lt;/div&gt;&lt;div class=&quot;mem-addr&quot;&gt;0x...c0&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;p class=&quot;til-p&quot; style=&quot;font-size:13px;color:#9ca3af&quot;&gt;int형 원소 하나는 4바이트이므로 주소가 4씩 증가하는 것을 확인할 수 있다.&lt;/p&gt;

  &lt;hr class=&quot;til-divider&quot;&gt;

  &lt;!-- ── 3. 배열 선언 및 초기화 ── --&gt;
  &lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;3&lt;/span&gt;배열 선언 및 초기화 — 1차원&lt;/div&gt;
  &lt;p class=&quot;til-p&quot;&gt;선언 문법은 &lt;code&gt;data_type array_name[size]&lt;/code&gt;다. 초기화 방식에 따라 동작이 달라지므로 주의해야 한다.&lt;/p&gt;

  &lt;div class=&quot;summary-grid&quot;&gt;
    &lt;div class=&quot;sum-box&quot;&gt;
      &lt;div class=&quot;sum-lbl&quot;&gt;초기화 없이 선언&lt;/div&gt;
      &lt;p&gt;&lt;strong&gt;쓰레기값(Garbage Value)&lt;/strong&gt;이 들어간다. 실행할 때마다 값이 달라지므로 반드시 초기화해야 한다.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&quot;sum-box&quot;&gt;
      &lt;div class=&quot;sum-lbl&quot;&gt;전체 초기화&lt;/div&gt;
      &lt;p&gt;&lt;code&gt;int arr[5] = {1,2,3,4,5}&lt;/code&gt;&lt;br&gt;앞에서부터 순서대로 값이 채워진다.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&quot;sum-box&quot;&gt;
      &lt;div class=&quot;sum-lbl&quot;&gt;부분 초기화&lt;/div&gt;
      &lt;p&gt;&lt;code&gt;int arr[5] = {1,2,3}&lt;/code&gt;&lt;br&gt;지정한 값 이후 나머지는 &lt;strong&gt;0으로 자동 초기화&lt;/strong&gt;된다.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&quot;sum-box&quot;&gt;
      &lt;div class=&quot;sum-lbl&quot;&gt;전체 0 초기화&lt;/div&gt;
      &lt;p&gt;&lt;code&gt;int arr[5] = {0}&lt;/code&gt; 또는 &lt;code&gt;int arr[5] = {}&lt;/code&gt;&lt;br&gt;모든 원소를 0으로 빠르게 초기화하는 관용 표현이다.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;til-warn&quot;&gt;&lt;p&gt;&lt;strong&gt;주의:&lt;/strong&gt; 초기화하지 않고 선언만 한 배열을 그대로 사용하면 쓰레기값이 출력된다. 코딩 테스트에서 원인 모를 오답의 주범이 될 수 있으므로, 선언과 동시에 초기화하는 습관을 들이자.&lt;/p&gt;&lt;/div&gt;

  &lt;hr class=&quot;til-divider&quot;&gt;

  &lt;!-- ── 4. 2차원 배열 ── --&gt;
  &lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;4&lt;/span&gt;2차원 배열 선언 및 메모리 구조&lt;/div&gt;
  &lt;p class=&quot;til-p&quot;&gt;2차원 배열은 &lt;strong&gt;1차원 배열을 여러 개 묶어 놓은 형태&lt;/strong&gt;로, &lt;code&gt;int arr[2][3]&lt;/code&gt;처럼 행(row)과 열(col) 크기를 함께 명시한다.&lt;/p&gt;

  &lt;!-- 2D 배열 시각화 --&gt;
  &lt;p class=&quot;til-p&quot; style=&quot;font-size:14px;margin-bottom:.4rem&quot;&gt;&lt;strong style=&quot;color:#f9fafb&quot;&gt;int arr[2][3] = {{1,2,3},{4,5,6}} — 논리 구조&lt;/strong&gt;&lt;/p&gt;
  &lt;div class=&quot;arr2d-wrap&quot;&gt;
    &lt;div style=&quot;display:flex;gap:8px;margin-bottom:4px;padding-left:48px&quot;&gt;
      &lt;div style=&quot;width:48px;text-align:center;font-size:11px;color:#6b7280;font-family:'JetBrains Mono',monospace&quot;&gt;[0]&lt;/div&gt;
      &lt;div style=&quot;width:48px;text-align:center;font-size:11px;color:#6b7280;font-family:'JetBrains Mono',monospace&quot;&gt;[1]&lt;/div&gt;
      &lt;div style=&quot;width:48px;text-align:center;font-size:11px;color:#6b7280;font-family:'JetBrains Mono',monospace&quot;&gt;[2]&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;arr2d-row&quot;&gt;
      &lt;div class=&quot;arr2d-lbl&quot;&gt;행 0&lt;/div&gt;
      &lt;div class=&quot;arr2d-cell&quot;&gt;1&lt;/div&gt;
      &lt;div class=&quot;arr2d-cell&quot;&gt;2&lt;/div&gt;
      &lt;div class=&quot;arr2d-cell&quot;&gt;3&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;arr2d-row&quot;&gt;
      &lt;div class=&quot;arr2d-lbl&quot;&gt;행 1&lt;/div&gt;
      &lt;div class=&quot;arr2d-cell&quot;&gt;4&lt;/div&gt;
      &lt;div class=&quot;arr2d-cell&quot;&gt;5&lt;/div&gt;
      &lt;div class=&quot;arr2d-cell&quot;&gt;6&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&quot;til-hl&quot;&gt;&lt;p&gt;&lt;strong&gt;행 우선(Row-Major) 저장:&lt;/strong&gt; C++에서 2차원 배열은 내부적으로 &lt;strong&gt;1행 전체 → 2행 전체&lt;/strong&gt; 순서로 연속 메모리에 저장된다. 즉 &lt;code&gt;arr[0][0], arr[0][1], arr[0][2], arr[1][0], arr[1][1], arr[1][2]&lt;/code&gt; 순으로 배치된다. 2차원처럼 보여도 메모리는 항상 1차원이다.&lt;/p&gt;&lt;/div&gt;

  &lt;div class=&quot;code-block&quot;&gt;&lt;pre&gt;
&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt; arr[&lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;][&lt;span class=&quot;cn&quot;&gt;3&lt;/span&gt;] = {
    {&lt;span class=&quot;cn&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;cn&quot;&gt;3&lt;/span&gt;},   &lt;span class=&quot;cc&quot;&gt;// 0행&lt;/span&gt;
    {&lt;span class=&quot;cn&quot;&gt;4&lt;/span&gt;, &lt;span class=&quot;cn&quot;&gt;5&lt;/span&gt;, &lt;span class=&quot;cn&quot;&gt;6&lt;/span&gt;}    &lt;span class=&quot;cc&quot;&gt;// 1행&lt;/span&gt;
};
&lt;span class=&quot;cc&quot;&gt;// 메모리: [1][2][3][4][5][6] 연속 배치&lt;/span&gt;
&lt;span class=&quot;cc&quot;&gt;// arr[1][0]의 주소 = arr[0][0]의 주소 + (3 * sizeof(int))&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

  &lt;hr class=&quot;til-divider&quot;&gt;

  &lt;!-- ── 5. 삽입 효율성 ── --&gt;
  &lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;5&lt;/span&gt;삽입 위치에 따른 시간 복잡도 차이&lt;/div&gt;
  &lt;p class=&quot;til-p&quot;&gt;배열은 연속 메모리 구조이기 때문에 삽입 위치에 따라 성능이 크게 달라진다.&lt;/p&gt;

  &lt;div class=&quot;two-col&quot;&gt;
    &lt;div class=&quot;vs-card&quot;&gt;
      &lt;div class=&quot;vc-hd vc-green&quot;&gt;맨 뒤 삽입 &amp;nbsp;&lt;span class=&quot;tag-good&quot;&gt;O(1)&lt;/span&gt;&lt;/div&gt;
      &lt;div class=&quot;big&quot;&gt;가장 효율적&lt;/div&gt;
      &lt;p&gt;기존 원소를 전혀 건드리지 않고 다음 빈 자리에 바로 추가. 다른 원소 이동이 없으므로 상수 시간.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&quot;vs-card&quot;&gt;
      &lt;div class=&quot;vc-hd vc-bad&quot;&gt;맨 앞 삽입 &amp;nbsp;&lt;span class=&quot;tag-bad&quot;&gt;O(N)&lt;/span&gt;&lt;/div&gt;
      &lt;div class=&quot;big&quot;&gt;가장 비효율적&lt;/div&gt;
      &lt;p&gt;기존 원소 전체를 한 칸씩 뒤로 밀어야 한다. 원소가 N개면 N번의 이동이 필요.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- 맨 뒤 삽입 시각화 --&gt;
  &lt;p class=&quot;til-p&quot; style=&quot;font-size:13px;color:#9ca3af;margin-bottom:.3rem&quot;&gt;맨 뒤 삽입 — arr[size]에 바로 대입&lt;/p&gt;
  &lt;div class=&quot;insert-vis&quot;&gt;
    &lt;div class=&quot;iv-cell iv-exist&quot;&gt;1&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-exist&quot;&gt;3&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-exist&quot;&gt;5&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-new&quot;&gt;99&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-empty&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-empty&quot;&gt;&lt;/div&gt;
    &lt;span class=&quot;iv-label&quot;&gt;← 한 번에 추가 O(1)&lt;/span&gt;
  &lt;/div&gt;

  &lt;!-- 맨 앞 삽입 시각화 --&gt;
  &lt;p class=&quot;til-p&quot; style=&quot;font-size:13px;color:#9ca3af;margin-bottom:.3rem&quot;&gt;맨 앞 삽입 — 기존 원소 전체 이동&lt;/p&gt;
  &lt;div class=&quot;insert-vis&quot;&gt;
    &lt;div class=&quot;iv-cell iv-new&quot;&gt;2&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-exist&quot;&gt;1&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-exist&quot;&gt;3&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-exist&quot;&gt;5&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-exist&quot;&gt;7&lt;/div&gt;
    &lt;div class=&quot;iv-cell iv-exist&quot;&gt;9&lt;/div&gt;
    &lt;span class=&quot;iv-label&quot;&gt;← 5번 이동 후 삽입 O(N)&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class=&quot;chart-title&quot; style=&quot;margin-top:1.2rem&quot;&gt;맨 뒤 vs 맨 앞 삽입 — 원소 수별 연산 횟수&lt;/div&gt;
  &lt;div class=&quot;chart-wrap&quot; style=&quot;height:180px&quot;&gt;
    &lt;canvas id=&quot;chartInsert&quot;&gt;&lt;/canvas&gt;
  &lt;/div&gt;
  &lt;div class=&quot;legend-row&quot;&gt;
    &lt;span class=&quot;legend-item&quot;&gt;&lt;span class=&quot;legend-dot&quot; style=&quot;background:#10b981&quot;&gt;&lt;/span&gt;맨 뒤 삽입 — O(1)&lt;/span&gt;
    &lt;span class=&quot;legend-item&quot;&gt;&lt;span class=&quot;legend-dot&quot; style=&quot;background:#ef4444&quot;&gt;&lt;/span&gt;맨 앞 삽입 — O(N)&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class=&quot;code-block&quot;&gt;&lt;pre&gt;
&lt;span class=&quot;cc&quot;&gt;// 맨 뒤 삽입 — O(1)&lt;/span&gt;
&lt;span class=&quot;ck&quot;&gt;if&lt;/span&gt; (size &amp;lt; capacity) {
    arr[size] = newValue;  &lt;span class=&quot;cc&quot;&gt;// 그냥 넣기만 하면 끝&lt;/span&gt;
    size++;
}

&lt;span class=&quot;cc&quot;&gt;// 맨 앞 삽입 — O(N)&lt;/span&gt;
&lt;span class=&quot;ck&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt; i = size - &lt;span class=&quot;cn&quot;&gt;1&lt;/span&gt;; i &amp;gt;= &lt;span class=&quot;cn&quot;&gt;0&lt;/span&gt;; i--)
    arr[i + &lt;span class=&quot;cn&quot;&gt;1&lt;/span&gt;] = arr[i];  &lt;span class=&quot;cc&quot;&gt;// 전체를 한 칸씩 뒤로&lt;/span&gt;
arr[&lt;span class=&quot;cn&quot;&gt;0&lt;/span&gt;] = newValue;
size++;&lt;/pre&gt;&lt;/div&gt;

  &lt;hr class=&quot;til-divider&quot;&gt;

  &lt;!-- ── 6. 실전 문제 풀이 패턴 ── --&gt;
  &lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;6&lt;/span&gt;실전 문제 — 배열 활용 패턴 3가지&lt;/div&gt;

  &lt;p class=&quot;til-p&quot; style=&quot;font-size:14px;margin-bottom:.6rem&quot;&gt;&lt;strong style=&quot;color:#f9fafb&quot;&gt;① 배열 제어하기 — 중복 제거 + 내림차순 정렬&lt;/strong&gt;&lt;/p&gt;
  &lt;div class=&quot;til-hl&quot;&gt;&lt;p&gt;&lt;strong&gt;핵심 아이디어:&lt;/strong&gt; &lt;code&gt;sort()&lt;/code&gt;로 내림차순 정렬 후 &lt;code&gt;unique()&lt;/code&gt;로 연속된 중복값을 뒤로 밀고, &lt;code&gt;erase()&lt;/code&gt;로 제거한다. &lt;code&gt;unique()&lt;/code&gt;는 정렬된 배열에서만 올바르게 동작하므로 &lt;strong&gt;순서가 중요&lt;/strong&gt;하다.&lt;/p&gt;&lt;/div&gt;
  &lt;div class=&quot;code-block&quot;&gt;&lt;pre&gt;
&lt;span class=&quot;ck&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;compare&lt;/span&gt;(&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt; a, &lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt; b) { &lt;span class=&quot;ck&quot;&gt;return&lt;/span&gt; a &amp;gt; b; }  &lt;span class=&quot;cc&quot;&gt;// 내림차순 기준&lt;/span&gt;

vector&amp;lt;&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt;&amp;gt; &lt;span class=&quot;cf&quot;&gt;solution&lt;/span&gt;(vector&amp;lt;&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt;&amp;gt; lst) {
    &lt;span class=&quot;cf&quot;&gt;sort&lt;/span&gt;(lst.begin(), lst.end(), compare);       &lt;span class=&quot;cc&quot;&gt;// 내림차순 정렬&lt;/span&gt;
    lst.&lt;span class=&quot;cf&quot;&gt;erase&lt;/span&gt;(&lt;span class=&quot;cf&quot;&gt;unique&lt;/span&gt;(lst.begin(), lst.end()),    &lt;span class=&quot;cc&quot;&gt;// 중복 제거&lt;/span&gt;
               lst.end());
    &lt;span class=&quot;ck&quot;&gt;return&lt;/span&gt; lst;
}
&lt;span class=&quot;cc&quot;&gt;// {4,2,2,1,1,3,4} → {4,3,2,1}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

  &lt;p class=&quot;til-p&quot; style=&quot;font-size:14px;margin-top:1.2rem;margin-bottom:.6rem&quot;&gt;&lt;strong style=&quot;color:#f9fafb&quot;&gt;② 두 수를 뽑아서 더하기 — set으로 중복 없는 합 수집&lt;/strong&gt;&lt;/p&gt;
  &lt;div class=&quot;til-hl&quot;&gt;&lt;p&gt;&lt;strong&gt;핵심 아이디어:&lt;/strong&gt; &lt;code&gt;set&amp;lt;int&amp;gt;&lt;/code&gt;에 두 원소의 합을 삽입하면 중복이 자동 제거되고 오름차순 정렬도 자동으로 된다. 이중 루프로 모든 쌍을 순회하면 O(N²).&lt;/p&gt;&lt;/div&gt;
  &lt;div class=&quot;code-block&quot;&gt;&lt;pre&gt;
vector&amp;lt;&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt;&amp;gt; &lt;span class=&quot;cf&quot;&gt;solution&lt;/span&gt;(vector&amp;lt;&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt;&amp;gt; numbers) {
    set&amp;lt;&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt;&amp;gt; sum;
    &lt;span class=&quot;ck&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;cn&quot;&gt;0&lt;/span&gt;; i &amp;lt; numbers.size(); ++i)
        &lt;span class=&quot;ck&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt; j = i + &lt;span class=&quot;cn&quot;&gt;1&lt;/span&gt;; j &amp;lt; numbers.size(); ++j)
            sum.insert(numbers[i] + numbers[j]);  &lt;span class=&quot;cc&quot;&gt;// set이 중복+정렬 처리&lt;/span&gt;
    &lt;span class=&quot;ck&quot;&gt;return&lt;/span&gt; vector&amp;lt;&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt;&amp;gt;(sum.begin(), sum.end());
}
&lt;span class=&quot;cc&quot;&gt;// {2,1,3,4,1} → {2,3,4,5,6,7}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

  &lt;p class=&quot;til-p&quot; style=&quot;font-size:14px;margin-top:1.2rem;margin-bottom:.6rem&quot;&gt;&lt;strong style=&quot;color:#f9fafb&quot;&gt;③ 모의고사 — 패턴 배열 + 나머지 연산&lt;/strong&gt;&lt;/p&gt;
  &lt;div class=&quot;til-hl&quot;&gt;&lt;p&gt;&lt;strong&gt;핵심 아이디어:&lt;/strong&gt; 각 수포자의 반복 패턴을 배열로 정의하고 &lt;code&gt;i % pattern.size()&lt;/code&gt;로 패턴을 순환시킨다. 맞힌 횟수를 비교해 최댓값과 동률인 사람을 모두 반환한다.&lt;/p&gt;&lt;/div&gt;
  &lt;div class=&quot;code-block&quot;&gt;&lt;pre&gt;
vector&amp;lt;&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt;&amp;gt; p1 = {&lt;span class=&quot;cn&quot;&gt;1&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;3&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;4&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;5&lt;/span&gt;};
vector&amp;lt;&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt;&amp;gt; p2 = {&lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;1&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;3&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;4&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;5&lt;/span&gt;};
vector&amp;lt;&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt;&amp;gt; p3 = {&lt;span class=&quot;cn&quot;&gt;3&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;3&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;1&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;1&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;4&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;4&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;5&lt;/span&gt;,&lt;span class=&quot;cn&quot;&gt;5&lt;/span&gt;};

&lt;span class=&quot;ck&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;ck&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;cn&quot;&gt;0&lt;/span&gt;; i &amp;lt; answers.size(); i++) {
    &lt;span class=&quot;ck&quot;&gt;if&lt;/span&gt; (answers[i] == p1[i % p1.size()]) cnt[&lt;span class=&quot;cn&quot;&gt;0&lt;/span&gt;]++;
    &lt;span class=&quot;ck&quot;&gt;if&lt;/span&gt; (answers[i] == p2[i % p2.size()]) cnt[&lt;span class=&quot;cn&quot;&gt;1&lt;/span&gt;]++;
    &lt;span class=&quot;ck&quot;&gt;if&lt;/span&gt; (answers[i] == p3[i % p3.size()]) cnt[&lt;span class=&quot;cn&quot;&gt;2&lt;/span&gt;]++;
}
&lt;span class=&quot;cc&quot;&gt;// i % size() 로 패턴을 무한 반복시키는 것이 핵심&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

  &lt;hr class=&quot;til-divider&quot;&gt;

  &lt;!-- ── 7. 정리 ── --&gt;
  &lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;7&lt;/span&gt;한눈에 보는 정리&lt;/div&gt;
  &lt;table class=&quot;recap-table&quot;&gt;
    &lt;thead&gt;
      &lt;tr&gt;&lt;th&gt;연산&lt;/th&gt;&lt;th&gt;시간 복잡도&lt;/th&gt;&lt;th&gt;이유&lt;/th&gt;&lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;임의 접근 &lt;code&gt;arr[i]&lt;/code&gt;&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;tag-good&quot;&gt;O(1)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;시작 주소 + i × 타입 크기로 직접 계산&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;맨 뒤 삽입/삭제&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;tag-good&quot;&gt;O(1)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;다른 원소 이동 없음&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;중간/맨 앞 삽입/삭제&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;tag-bad&quot;&gt;O(N)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;이후 원소들을 전부 밀거나 당겨야 함&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;선형 탐색&lt;/td&gt;
        &lt;td&gt;&lt;span class=&quot;tag-bad&quot;&gt;O(N)&lt;/span&gt;&lt;/td&gt;
        &lt;td&gt;최악의 경우 모든 원소를 순회&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td&gt;크기 변경&lt;/td&gt;
        &lt;td&gt;불가&lt;/td&gt;
        &lt;td&gt;C++ 정적 배열은 선언 시 크기 고정&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;hr class=&quot;til-divider&quot;&gt;

  &lt;!-- ── 8. 회고 ── --&gt;
  &lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;8&lt;/span&gt;오늘의 회고&lt;/div&gt;
  &lt;div class=&quot;til-card&quot;&gt;
    &lt;p&gt;이번 챕터에서 가장 인상 깊었던 건 &lt;strong&gt;2차원 배열도 메모리상에서는 1차원으로 저장된다&lt;/strong&gt;는 사실이었다. 행 우선 순서로 연속 배치된다는 것을 이해하니, 2차원 배열의 열(col) 방향 순회가 왜 행(row) 방향 순회보다 느린지 — 캐시 미스가 더 자주 발생하기 때문 — 까지 자연스럽게 이해됐다.&lt;/p&gt;
    &lt;p&gt;맨 앞 삽입이 O(N)이라는 사실을 단순히 외우는 것에서 끝내지 않고, 연속 메모리 구조상 앞의 빈 공간이 없기 때문에 전체를 밀어야 한다는 &lt;strong&gt;이유&lt;/strong&gt;를 이해한 것이 중요했다. 앞쪽 삽입/삭제가 잦은 경우에는 배열 대신 &lt;code&gt;deque&lt;/code&gt;를 선택해야 한다는 판단 기준도 이 이해에서 나온다.&lt;/p&gt;
  &lt;/div&gt;

&lt;/div&gt;

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
(function(){
  var gridC  = 'rgba(255,255,255,0.07)';
  var labelC = '#6b7280';

  new Chart(document.getElementById('chartInsert'), {
    type: 'bar',
    data: {
      labels: ['N=100', 'N=1,000', 'N=10,000', 'N=100,000'],
      datasets: [
        {
          label: '맨 뒤 삽입',
          data: [1, 1, 1, 1],
          backgroundColor: '#10b981',
          borderRadius: 5,
          borderSkipped: false
        },
        {
          label: '맨 앞 삽입',
          data: [100, 1000, 10000, 100000],
          backgroundColor: '#ef4444',
          borderRadius: 5,
          borderSkipped: false
        }
      ]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        legend: { display: false },
        tooltip: {
          callbacks: {
            label: function(ctx){
              return ' ' + ctx.dataset.label + ': ' + ctx.parsed.y.toLocaleString() + '회';
            }
          }
        }
      },
      scales: {
        x: { grid: { color: gridC }, ticks: { color: labelC, font: { size: 12 } } },
        y: {
          type: 'logarithmic',
          grid: { color: gridC },
          ticks: {
            color: labelC,
            font: { size: 12 },
            callback: function(v){
              if ([1,10,100,1000,10000,100000].includes(v))
                return v &gt;= 1000 ? (v/1000)+'k' : v;
              return '';
            }
          }
        }
      }
    }
  });
})();
&lt;/script&gt;</description>
      <category>프로그래밍/코드카타</category>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/101</guid>
      <comments>https://hanong8.tistory.com/101#entry101comment</comments>
      <pubDate>Thu, 7 May 2026 20:17:35 +0900</pubDate>
    </item>
    <item>
      <title>[C++] 재귀 함수를 코드로</title>
      <link>https://hanong8.tistory.com/100</link>
      <description>&lt;div&gt;
&lt;style&gt;
  .til-wrap {
    max-width: 720px;
    margin: 0 auto;
    font-family: 'Noto Sans KR', 'Apple SD Gothic Neo', sans-serif;
    color: #e5e7eb;
    line-height: 1.8;
    padding: 0 0 3rem;
  }
  .til-divider {
    border: none;
    border-top: 1px solid rgba(255,255,255,0.1);
    margin: 1.8rem 0;
  }
  .til-section-hd {
    font-size: 18px;
    font-weight: 700;
    color: #f9fafb;
    margin-bottom: 1rem;
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .til-num {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.15);
    font-size: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #9ca3af;
    font-weight: 700;
    flex-shrink: 0;
  }
  .til-p {
    font-size: 15px;
    line-height: 1.85;
    color: #d1d5db;
    margin-bottom: .9rem;
  }
  .til-p strong { color: #f9fafb; }
  code {
    font-family: 'JetBrains Mono', 'D2Coding', 'Fira Code', monospace;
    font-size: 13px;
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.12);
    border-radius: 4px;
    padding: 1px 6px;
    color: #93c5fd;
  }
  .til-card {
    background: rgba(255,255,255,0.05);
    border-radius: 10px;
    padding: 1rem 1.2rem;
    margin: .8rem 0;
    border: 1px solid rgba(255,255,255,0.08);
  }
  .til-card p {
    margin: 0;
    font-size: 14px;
    color: #d1d5db;
    line-height: 1.75;
  }
  .til-card p + p { margin-top: .5rem; }
  .til-card p strong { color: #f9fafb; }
  .til-hl {
    border-left: 3px solid #60a5fa;
    padding: .75rem 1.1rem;
    background: rgba(59,130,246,0.1);
    border-radius: 0 8px 8px 0;
    margin: .9rem 0;
  }
  .til-hl p {
    margin: 0;
    font-size: 14px;
    color: #bfdbfe;
    line-height: 1.75;
  }
  .til-hl p strong { color: #93c5fd; }
  .til-warn {
    background: rgba(251,191,36,0.08);
    border: 1px solid rgba(251,191,36,0.25);
    border-radius: 8px;
    padding: .75rem 1.1rem;
    margin: .8rem 0;
  }
  .til-warn p {
    margin: 0;
    font-size: 13px;
    color: #fde68a;
    line-height: 1.7;
  }
  .til-warn p strong { color: #fcd34d; }
  .til-danger {
    background: rgba(239,68,68,0.08);
    border: 1px solid rgba(239,68,68,0.25);
    border-radius: 8px;
    padding: .75rem 1.1rem;
    margin: .8rem 0;
  }
  .til-danger p {
    margin: 0;
    font-size: 13px;
    color: #fca5a5;
    line-height: 1.7;
  }
  .til-danger p strong { color: #f87171; }
  .two-col {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
    margin: .9rem 0;
  }
  .vs-card {
    background: rgba(255,255,255,0.04);
    border: 1px solid rgba(255,255,255,0.1);
    border-radius: 10px;
    padding: 1rem 1.1rem;
  }
  .vc-hd {
    font-size: 12px;
    font-weight: 700;
    font-family: 'JetBrains Mono', monospace;
    margin-bottom: 7px;
    letter-spacing: .03em;
  }
  .vc-blue  { color: #93c5fd; }
  .vc-green { color: #6ee7b7; }
  .vc-bad   { color: #fca5a5; }
  .vs-card .big {
    font-size: 17px;
    font-weight: 700;
    color: #f9fafb;
    margin-bottom: 5px;
  }
  .vs-card p {
    margin: 0;
    font-size: 13px;
    line-height: 1.6;
    color: #9ca3af;
  }
  .summary-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
    margin: .9rem 0;
  }
  .sum-box {
    background: rgba(255,255,255,0.04);
    border-radius: 8px;
    padding: .8rem 1rem;
    border: 1px solid rgba(255,255,255,0.08);
  }
  .sum-lbl {
    font-size: 11px;
    font-weight: 700;
    color: #6b7280;
    letter-spacing: .06em;
    margin-bottom: 5px;
    text-transform: uppercase;
  }
  .sum-box p {
    margin: 0;
    font-size: 13px;
    line-height: 1.6;
    color: #d1d5db;
  }
  .sum-box p strong { color: #f9fafb; }
  .chart-title {
    font-size: 12px;
    font-weight: 700;
    color: #6b7280;
    margin-bottom: 6px;
    letter-spacing: .04em;
    text-transform: uppercase;
  }
  .chart-wrap {
    position: relative;
    width: 100%;
    margin: .9rem 0 .4rem;
  }
  .legend-row {
    display: flex;
    gap: 16px;
    font-size: 12px;
    color: #6b7280;
    margin-bottom: .8rem;
    flex-wrap: wrap;
  }
  .legend-item {
    display: flex;
    align-items: center;
    gap: 5px;
  }
  .legend-dot {
    width: 10px;
    height: 10px;
    border-radius: 2px;
    flex-shrink: 0;
  }
  .recap-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
    margin: .9rem 0;
  }
  .recap-table th {
    background: rgba(255,255,255,0.07);
    padding: 10px 14px;
    text-align: left;
    font-weight: 700;
    color: #f9fafb;
    border: 1px solid rgba(255,255,255,0.1);
    font-size: 13px;
  }
  .recap-table td {
    padding: 9px 14px;
    border: 1px solid rgba(255,255,255,0.08);
    color: #d1d5db;
    vertical-align: top;
  }
  .recap-table tr:nth-child(even) td {
    background: rgba(255,255,255,0.03);
  }
  .tag-good {
    display: inline-block;
    background: rgba(16,185,129,0.15);
    color: #6ee7b7;
    font-size: 11px;
    font-weight: 700;
    padding: 2px 8px;
    border-radius: 12px;
  }
  .tag-bad {
    display: inline-block;
    background: rgba(239,68,68,0.15);
    color: #fca5a5;
    font-size: 11px;
    font-weight: 700;
    padding: 2px 8px;
    border-radius: 12px;
  }
  /* 코드 블록 */
  .code-block {
    background: rgba(0,0,0,0.3);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 10px;
    padding: 1rem 1.2rem;
    margin: .8rem 0;
    overflow-x: auto;
  }
  .code-block pre {
    margin: 0;
    font-family: 'JetBrains Mono', 'D2Coding', 'Fira Code', monospace;
    font-size: 13px;
    line-height: 1.75;
    color: #d1fae5;
    white-space: pre;
  }
  .ck  { color: #93c5fd; }   /* keyword */
  .cf  { color: #86efac; }   /* function */
  .cn  { color: #fbbf24; }   /* number */
  .cc  { color: #6b7280; font-style: italic; } /* comment */
  .cs  { color: #fca5a5; }   /* string */
  /* 스텝 리스트 */
  .step-list { margin: .9rem 0; }
  .step-item {
    display: flex;
    gap: 12px;
    align-items: flex-start;
    margin-bottom: .75rem;
  }
  .step-badge {
    width: 22px; height: 22px;
    border-radius: 50%;
    background: rgba(96,165,250,0.2);
    border: 1px solid rgba(96,165,250,0.4);
    color: #93c5fd;
    font-size: 11px;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    margin-top: 3px;
    font-family: 'JetBrains Mono', monospace;
  }
  .step-text {
    font-size: 14px;
    color: #d1d5db;
    line-height: 1.7;
  }
  .step-text strong { color: #f9fafb; }
  /* 수식 */
  .math-box {
    background: rgba(0,0,0,0.25);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 8px;
    padding: .7rem 1.2rem;
    margin: .8rem 0;
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px;
    color: #fde68a;
    line-height: 2;
    text-align: center;
  }
  /* 스택 시각화 */
  .stack-vis {
    display: flex;
    gap: 20px;
    align-items: flex-end;
    margin: 1rem 0 .4rem;
    overflow-x: auto;
    padding-bottom: 4px;
  }
  .stack-col { display: flex; flex-direction: column; align-items: center; gap: 3px; }
  .stack-lbl { font-size: 11px; color: #6b7280; margin-bottom: 6px; white-space: nowrap; }
  .sf {
    width: 100px;
    padding: 5px 0;
    text-align: center;
    font-size: 12px;
    font-family: 'JetBrains Mono', monospace;
    border-radius: 4px;
    background: rgba(96,165,250,0.12);
    border: 1px solid rgba(96,165,250,0.25);
    color: #bfdbfe;
  }
  .sf.main {
    background: rgba(167,139,250,0.12);
    border-color: rgba(167,139,250,0.3);
    color: #ddd6fe;
  }
  .stack-arrow {
    color: #4b5563;
    font-size: 18px;
    padding-bottom: 16px;
    flex-shrink: 0;
  }
&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;til-wrap&quot;&gt;&lt;!-- ────────────────── 1. 핵심 메시지 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;1&lt;/span&gt;이번 챕터의 핵심 메시지&lt;/div&gt;
&lt;p class=&quot;til-p&quot; data-ke-size=&quot;size16&quot;&gt;재귀는 &lt;b&gt;자기 자신을 호출하는 함수&lt;/b&gt;로, 올바르게 설계하지 않으면 스택 오버플로우가 발생한다. 핵심은 단 두 가지 &amp;mdash; &lt;b&gt;기저 조건(Base Case)&lt;/b&gt;과 &lt;b&gt;재귀 호출 단계(Recursive Step)&lt;/b&gt;를 명확히 정의하는 것이다. 이 두 가지가 갖춰지면 팩토리얼, DFS, 분할 정복까지 재귀로 자연스럽게 표현할 수 있다.&lt;/p&gt;
&lt;hr class=&quot;til-divider&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- ────────────────── 2. 재귀의 정의와 요건 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;2&lt;/span&gt;재귀의 정의와 두 가지 요건&lt;/div&gt;
&lt;p class=&quot;til-p&quot; data-ke-size=&quot;size16&quot;&gt;재귀 함수를 구현하려면 반드시 아래 두 가지를 설계해야 한다. 하나라도 빠지면 무한 루프나 스택 오버플로우로 이어진다.&lt;/p&gt;
&lt;div class=&quot;two-col&quot;&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-blue&quot;&gt;① 재귀 호출 단계&lt;/div&gt;
&lt;div class=&quot;big&quot;&gt;문제 크기 축소&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기 자신을 호출하되, 매번 &lt;b&gt;문제의 크기를 줄여야&lt;/b&gt; 한다. 크기가 줄지 않으면 무한 호출로 이어진다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-green&quot;&gt;② 기저 조건 (Base Case)&lt;/div&gt;
&lt;div class=&quot;big&quot;&gt;종료 조건&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 조건을 만족하면 자기 자신을 더 이상 호출하지 않고 &lt;b&gt;직접 값을 반환&lt;/b&gt;해 함수를 종료한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;til-danger&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기저 조건이 없으면?&lt;/b&gt; 함수가 종료되지 않고 계속 스택 프레임이 쌓여 &lt;b&gt;스택 오버플로우(Stack Overflow)&lt;/b&gt;가 발생한다. 종료 조건은 반드시 명확하게 정의해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;hr class=&quot;til-divider&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- ────────────────── 3. 재귀 함수 설계 — 팩토리얼 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;3&lt;/span&gt;재귀 함수 설계 &amp;mdash; 팩토리얼 예시&lt;/div&gt;
&lt;p class=&quot;til-p&quot; data-ke-size=&quot;size16&quot;&gt;재귀 함수를 설계할 때는 &lt;b&gt;&quot;구현하려는 함수가 이미 있다고 가정&quot;&lt;/b&gt;하는 사고방식이 핵심이다. 그러면 현재 문제를 더 작은 문제의 해로 표현하는 데 집중할 수 있다.&lt;/p&gt;
&lt;div class=&quot;step-list&quot;&gt;
&lt;div class=&quot;step-item&quot;&gt;
&lt;div class=&quot;step-badge&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;step-text&quot;&gt;&lt;b&gt;문제 정의:&lt;/b&gt; &lt;code&gt;Fact(N)&lt;/code&gt;은 N!을 반환하는 함수라고 가정한다.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step-item&quot;&gt;
&lt;div class=&quot;step-badge&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;step-text&quot;&gt;&lt;b&gt;재귀 호출 단계:&lt;/b&gt; &lt;code&gt;Fact(N) = N &amp;times; Fact(N-1)&lt;/code&gt; &amp;rarr; 크기 N 문제를 크기 N-1 문제로 축소한다.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;step-item&quot;&gt;
&lt;div class=&quot;step-badge&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;step-text&quot;&gt;&lt;b&gt;기저 조건:&lt;/b&gt; N이 0 또는 1이면 더 이상 쪼갤 수 없으므로 1을 직접 반환한다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;math-box&quot;&gt;n! = 1, if n = 0 or n = 1 &amp;larr; 기저 조건&lt;br /&gt;n! = n &amp;times; (n&amp;minus;1)!, if n &amp;gt; 1 &amp;larr; 재귀 호출 단계&lt;/div&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int Fact(int N) {
    if (N == 0 || N == 1) return 1;  // 기저 조건
    return N * Fact(N - 1);           // 재귀 호출 단계
}
// Fact(5) &amp;rarr; 120&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;til-p&quot; style=&quot;font-size: 14px; margin-bottom: .4rem;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스택 호출 흐름 (N=5)&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Fact(5) 호출
  Fact(4) 호출
    Fact(3) 호출
      Fact(2) 호출
        Fact(1) 호출
          Fact(0) 호출
          Fact(0) = 1 반환  &amp;larr; 기저 조건 도달
        Fact(1) = 1 반환
      Fact(2) = 2 반환
    Fact(3) = 6 반환
  Fact(4) = 24 반환
Fact(5) = 120 반환&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr class=&quot;til-divider&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- ────────────────── 4. 수학적 귀납법과 재귀 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;4&lt;/span&gt;수학적 귀납법과 재귀의 관계&lt;/div&gt;
&lt;p class=&quot;til-p&quot; data-ke-size=&quot;size16&quot;&gt;재귀 함수 설계는 &lt;b&gt;수학적 귀납법(Mathematical Induction)&lt;/b&gt;과 동일한 논리 구조를 가진다. 도미노 효과를 떠올리면 쉽다.&lt;/p&gt;
&lt;div class=&quot;two-col&quot;&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-blue&quot;&gt;수학적 귀납법&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 단계:&lt;/b&gt; 첫 번째 도미노가 넘어진다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;귀납 단계:&lt;/b&gt; k번째가 넘어지면 k+1번째도 반드시 넘어진다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-green&quot;&gt;재귀 함수 설계&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기저 조건:&lt;/b&gt; 가장 작은 입력(0 or 1)은 직접 답을 반환한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;재귀 호출 단계:&lt;/b&gt; 크기 N 문제를 크기 N-1 문제로 표현한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;til-hl&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 인사이트:&lt;/b&gt; 구현하려는 함수가 이미 있다고 가정하면, 더 작은 문제로 현재 문제를 표현하는 데만 집중할 수 있다. 이것이 재귀 설계의 본질이다.&lt;/p&gt;
&lt;/div&gt;
&lt;hr class=&quot;til-divider&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- ────────────────── 5. 메모리 구조 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;5&lt;/span&gt;재귀 함수 동작 시 메모리의 모습&lt;/div&gt;
&lt;p class=&quot;til-p&quot; data-ke-size=&quot;size16&quot;&gt;함수가 호출될 때마다 &lt;b&gt;스택 프레임(Stack Frame)&lt;/b&gt;이 생성되어 쌓인다. 스택 프레임에는 지역 변수, 매개 변수, 반환 주소가 포함된다. 함수가 종료되면 해당 프레임이 제거되고 이전 호출 위치로 돌아간다.&lt;/p&gt;
&lt;!-- 스택 시각화 --&gt;
&lt;div class=&quot;stack-vis&quot;&gt;
&lt;div class=&quot;stack-col&quot;&gt;
&lt;div class=&quot;stack-lbl&quot;&gt;초기&lt;/div&gt;
&lt;div class=&quot;sf main&quot;&gt;main()&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;stack-arrow&quot;&gt;&amp;rarr;&lt;/div&gt;
&lt;div class=&quot;stack-col&quot;&gt;
&lt;div class=&quot;stack-lbl&quot;&gt;Fact(5) 호출&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(5)&lt;/div&gt;
&lt;div class=&quot;sf main&quot;&gt;main()&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;stack-arrow&quot;&gt;&amp;rarr;&lt;/div&gt;
&lt;div class=&quot;stack-col&quot;&gt;
&lt;div class=&quot;stack-lbl&quot;&gt;Fact(3) 호출&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(3)&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(4)&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(5)&lt;/div&gt;
&lt;div class=&quot;sf main&quot;&gt;main()&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;stack-arrow&quot;&gt;&amp;rarr;&lt;/div&gt;
&lt;div class=&quot;stack-col&quot;&gt;
&lt;div class=&quot;stack-lbl&quot;&gt;최대 깊이&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(0)&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(1)&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(2)&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(3)&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(4)&lt;/div&gt;
&lt;div class=&quot;sf&quot;&gt;Fact(5)&lt;/div&gt;
&lt;div class=&quot;sf main&quot;&gt;main()&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;til-warn&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; 재귀 깊이가 깊어질수록 스택 메모리를 계속 소비한다. 기저 조건에 도달한 후에는 역방향으로 각 프레임이 차례로 제거되며 최종 결과가 계산된다. 호출 깊이를 적절히 제한하지 않으면 스택 오버플로우가 발생할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;hr class=&quot;til-divider&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- ────────────────── 6. 재귀가 효율적인 경우 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;6&lt;/span&gt;재귀가 효율적인 경우&lt;/div&gt;
&lt;p class=&quot;til-p&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 반복 작업은 반복문으로도 구현할 수 있다. 그럼에도 재귀가 더 자연스럽고 효율적인 경우가 있다.&lt;/p&gt;
&lt;div class=&quot;summary-grid&quot;&gt;
&lt;div class=&quot;sum-box&quot;&gt;
&lt;div class=&quot;sum-lbl&quot;&gt;스택 기반 알고리즘&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 호출 자체가 LIFO 스택 구조다. &lt;b&gt;DFS(깊이 우선 탐색)&lt;/b&gt;을 명시적 스택 없이 재귀로 자연스럽게 구현할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sum-box&quot;&gt;
&lt;div class=&quot;sum-lbl&quot;&gt;분할 정복 알고리즘&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 &lt;b&gt;분할 &amp;rarr; 정복 &amp;rarr; 결합&lt;/b&gt;하는 구조가 재귀와 맞아떨어진다. 병합 정렬, 퀵 정렬이 대표적이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;til-p&quot; style=&quot;font-size: 14px; margin-bottom: .4rem;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DFS &amp;mdash; 재귀 vs 반복문 비교&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;two-col&quot;&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-green&quot;&gt;재귀 DFS &amp;nbsp;&lt;span class=&quot;tag-good&quot;&gt;간결&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;code-block&quot; style=&quot;margin-top: 8px;&quot;&gt;
&lt;pre class=&quot;gradle&quot; style=&quot;margin: 0; font-family: 'JetBrains Mono',monospace; font-size: 12px; line-height: 1.7; color: #d1fae5; white-space: pre;&quot;&gt;&lt;code&gt;void DFS(int node) {
  visited[node] = true;
  cout &amp;lt;&amp;lt; node &amp;lt;&amp;lt; ' ';
  for (int next : graph[node])
    if (!visited[next])
      DFS(next); // 재귀
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-bad&quot;&gt;반복문 DFS &amp;nbsp;&lt;span class=&quot;tag-bad&quot;&gt;복잡&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;code-block&quot; style=&quot;margin-top: 8px;&quot;&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;margin: 0; font-family: 'JetBrains Mono',monospace; font-size: 12px; line-height: 1.7; color: #d1fae5; white-space: pre;&quot;&gt;&lt;code&gt;void DFS(int start) {
  stack&amp;lt;int&amp;gt; s;
  s.push(start);
  visited[start] = true;
  while (!s.empty()) {
    // 역순 탐색 별도 처리...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;til-p&quot; style=&quot;font-size: 14px; color: #9ca3af;&quot; data-ke-size=&quot;size16&quot;&gt;재귀 DFS는 각 함수가 자신과 인접한 노드만 관리하면 되므로 코드가 간결하고 가독성이 높다. 반면 반복문 DFS는 스택 초기화, 삽입, 삭제, 역순 탐색까지 직접 처리해야 해서 코드가 복잡해진다.&lt;/p&gt;
&lt;hr class=&quot;til-divider&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- ────────────────── 7. 재귀 주의점 &amp; 메모이제이션 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;7&lt;/span&gt;재귀 사용 시 주의점 &amp;mdash; 피보나치와 메모이제이션&lt;/div&gt;
&lt;p class=&quot;til-p&quot; data-ke-size=&quot;size16&quot;&gt;단순 재귀는 중복 호출 문제를 일으킬 수 있다. &lt;code&gt;fibo(6)&lt;/code&gt;을 단순 재귀로 계산하면 같은 값을 여러 번 반복 계산해 시간 복잡도가 &lt;b&gt;O(2ⁿ)&lt;/b&gt;까지 치솟는다.&lt;/p&gt;
&lt;div class=&quot;two-col&quot;&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-bad&quot;&gt;단순 재귀 &amp;nbsp;&lt;span class=&quot;tag-bad&quot;&gt;O(2ⁿ)&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 함수가 두 개를 호출 &amp;rarr; 호출 수가 지수적으로 폭증. &lt;code&gt;fibo(4)&lt;/code&gt;가 2번, &lt;code&gt;fibo(3)&lt;/code&gt;이 4번 중복 호출된다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-green&quot;&gt;메모이제이션 &amp;nbsp;&lt;span class=&quot;tag-good&quot;&gt;O(N)&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 계산한 결과를 배열에 저장. 이후 동일한 값을 요청받으면 &lt;b&gt;저장된 값을 즉시 반환&lt;/b&gt;해 중복 연산을 제거한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;chart-title&quot;&gt;fibo(40) 계산 &amp;mdash; 단순 재귀 vs 메모이제이션 호출 횟수&lt;/div&gt;
&lt;div class=&quot;chart-wrap&quot; style=&quot;height: 170px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;legend-row&quot;&gt;&lt;span class=&quot;legend-item&quot;&gt;&lt;span style=&quot;background: #ef4444;&quot; class=&quot;legend-dot&quot;&gt;&lt;/span&gt;단순 재귀 &amp;mdash; O(2ⁿ)&lt;/span&gt; &lt;span class=&quot;legend-item&quot;&gt;&lt;span style=&quot;background: #10b981;&quot; class=&quot;legend-dot&quot;&gt;&lt;/span&gt;메모이제이션 &amp;mdash; O(N)&lt;/span&gt;&lt;/div&gt;
&lt;p class=&quot;til-p&quot; style=&quot;font-size: 14px; color: #9ca3af;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;fibo(40)&lt;/code&gt; 기준으로 단순 재귀는 약 3억 3천만 번의 함수 호출이 발생하지만, 메모이제이션 적용 시 단 79번만 호출된다. 메모이제이션 하나만으로 &lt;b&gt;수백만 배&lt;/b&gt;의 성능 차이가 생긴다.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 메모이제이션을 적용한 피보나치
vector&amp;lt;int&amp;gt; memo(1000, -1);

int fibo(int n) {
    if (n == 1 || n == 2) return 1;
    if (memo[n] != -1) return memo[n];  // 이미 계산됨 &amp;rarr; 즉시 반환
    memo[n] = fibo(n - 1) + fibo(n - 2);
    return memo[n];
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr class=&quot;til-divider&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- ────────────────── 8. 지수 연산 — 분할 정복 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;8&lt;/span&gt;재귀 예시 &amp;mdash; 지수 연산의 최적화&lt;/div&gt;
&lt;p class=&quot;til-p&quot; data-ke-size=&quot;size16&quot;&gt;X의 N승을 단순 재귀로 구현하면 O(N)이다. N이 짝수일 때 문제 크기를 절반으로 줄이는 분할 정복을 적용하면 &lt;b&gt;O(log N)&lt;/b&gt;으로 개선할 수 있다.&lt;/p&gt;
&lt;div class=&quot;two-col&quot;&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-bad&quot;&gt;기본 재귀 &amp;nbsp;&lt;span class=&quot;tag-bad&quot;&gt;O(N)&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 1씩 줄여가며 N번 호출. N이 클수록 호출 횟수가 선형으로 증가한다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;vs-card&quot;&gt;
&lt;div class=&quot;vc-hd vc-green&quot;&gt;분할 정복 &amp;nbsp;&lt;span class=&quot;tag-good&quot;&gt;O(log N)&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짝수면 절반으로 분할해 제곱. 홀수면 하나 더 곱함. 호출 횟수가 log N으로 급감한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;chart-title&quot;&gt;pow(2, N) 계산 &amp;mdash; 재귀 호출 횟수 비교&lt;/div&gt;
&lt;div class=&quot;chart-wrap&quot; style=&quot;height: 170px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;legend-row&quot;&gt;&lt;span class=&quot;legend-item&quot;&gt;&lt;span style=&quot;background: #ef4444;&quot; class=&quot;legend-dot&quot;&gt;&lt;/span&gt;기본 재귀 &amp;mdash; O(N)&lt;/span&gt; &lt;span class=&quot;legend-item&quot;&gt;&lt;span style=&quot;background: #10b981;&quot; class=&quot;legend-dot&quot;&gt;&lt;/span&gt;분할 정복 &amp;mdash; O(log N)&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 분할 정복을 활용한 효율적인 X의 N승 계산
double pow(double X, int N) {
    if (N == 0) return 1;
    double half = pow(X, N / 2);
    if (N % 2 == 0)
        return half * half;        // 짝수: (X^(N/2))&amp;sup2;
    else
        return half * half * X;    // 홀수: (X^(N/2))&amp;sup2; &amp;times; X
}
// pow(2.0, 10) = 1024&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr class=&quot;til-divider&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- ────────────────── 9. 정리 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;9&lt;/span&gt;한눈에 보는 정리 &amp;mdash; 재귀 vs 반복문&lt;/div&gt;
&lt;table class=&quot;recap-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특성&lt;/th&gt;
&lt;th&gt;재귀 함수&lt;/th&gt;
&lt;th&gt;반복문&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;메모리 사용&lt;/td&gt;
&lt;td&gt;스택 프레임 누적 &amp;rarr; 오버플로우 위험&lt;/td&gt;
&lt;td&gt;스택 메모리 사용 없음 &amp;rarr; 효율적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 속도&lt;/td&gt;
&lt;td&gt;함수 호출 오버헤드 &amp;rarr; 상대적으로 느림&lt;/td&gt;
&lt;td&gt;함수 호출 없음 &amp;rarr; 일반적으로 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가독성&lt;/td&gt;
&lt;td&gt;재귀 구조 문제에서 간결하고 직관적&lt;/td&gt;
&lt;td&gt;단순 반복 작업에서 직관적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;변수 사용&lt;/td&gt;
&lt;td&gt;상태 변수 불필요, 코드 간결&lt;/td&gt;
&lt;td&gt;상태 유지 변수 다수 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;무한 반복 시&lt;/td&gt;
&lt;td&gt;스택 오버플로우 발생&lt;/td&gt;
&lt;td&gt;CPU 점유 &amp;rarr; 시스템 부하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적합한 상황&lt;/td&gt;
&lt;td&gt;DFS, 분할 정복, 트리/그래프 탐색&lt;/td&gt;
&lt;td&gt;단순 집계, 선형 순회, 성능 중시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr class=&quot;til-divider&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;!-- ────────────────── 10. 회고 ────────────────── --&gt;
&lt;div class=&quot;til-section-hd&quot;&gt;&lt;span class=&quot;til-num&quot;&gt;10&lt;/span&gt;오늘의 회고&lt;/div&gt;
&lt;div class=&quot;til-card&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 챕터에서 가장 인상 깊었던 건 &lt;b&gt;&quot;구현하려는 함수가 이미 있다고 가정하라&quot;&lt;/b&gt;는 사고방식이었다. 처음엔 순환 논리처럼 느껴졌지만, 수학적 귀납법과 연결해서 생각하니 오히려 명확하게 이해됐다. 재귀를 설계할 때 기저 조건을 먼저 잡고, 그다음 현재 문제를 더 작은 문제로 표현하는 순서로 접근하면 훨씬 수월하다는 것도 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피보나치의 단순 재귀가 &lt;b&gt;O(2ⁿ)&lt;/b&gt;이 되는 이유를 트리 구조로 직접 시각화하니 명확하게 와닿았고, 메모이제이션 하나만으로 O(N)으로 줄어드는 것이 생각보다 훨씬 강력하게 느껴졌다. 앞으로 재귀 문제를 만나면 ① 중복 호출이 발생하는 구조인지 확인하고, 그렇다면 메모이제이션을 바로 적용하는 습관을 들여야겠다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
(function(){
  var gridC  = 'rgba(255,255,255,0.07)';
  var labelC = '#6b7280';
  var baseOpts = {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
      legend: { display: false },
      tooltip: {
        callbacks: {
          label: function(ctx){ return ' ' + ctx.parsed.y.toLocaleString() + ' 회'; }
        }
      }
    },
    scales: {
      x: { grid: { color: gridC }, ticks: { color: labelC, font: { size: 12 } } },
      y: {
        type: 'logarithmic',
        grid: { color: gridC },
        ticks: {
          color: labelC,
          font: { size: 12 },
          callback: function(v){
            if ([1,10,100,1000,10000,100000,1000000,100000000,1000000000].includes(v)){
              if (v &gt;= 1000000) return (v/1000000).toFixed(0)+'M';
              if (v &gt;= 1000)    return (v/1000).toFixed(0)+'k';
              return v;
            }
            return '';
          }
        }
      }
    }
  };

  /* fibo 호출 횟수 차트 */
  new Chart(document.getElementById('chartFibo'), {
    type: 'bar',
    data: {
      labels: ['단순 재귀 (fibo 40)', '메모이제이션 (fibo 40)'],
      datasets: [{
        data: [331160281, 79],
        backgroundColor: ['#ef4444', '#10b981'],
        borderRadius: 5,
        borderSkipped: false
      }]
    },
    options: baseOpts
  });

  /* pow 호출 횟수 차트 */
  new Chart(document.getElementById('chartPow'), {
    type: 'bar',
    data: {
      labels: ['기본 재귀 (N=1024)', '분할 정복 (N=1024)'],
      datasets: [{
        data: [1024, 21],
        backgroundColor: ['#ef4444', '#10b981'],
        borderRadius: 5,
        borderSkipped: false
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        legend: { display: false },
        tooltip: {
          callbacks: {
            label: function(ctx){ return ' ' + ctx.parsed.y.toLocaleString() + ' 회'; }
          }
        }
      },
      scales: {
        x: { grid: { color: gridC }, ticks: { color: labelC, font: { size: 12 } } },
        y: {
          type: 'logarithmic',
          grid: { color: gridC },
          ticks: {
            color: labelC,
            font: { size: 12 },
            callback: function(v){
              if ([1,10,100,1000,10000].includes(v)) return v &gt;= 1000 ? (v/1000)+'k' : v;
              return '';
            }
          }
        }
      }
    }
  });
})();
&lt;/script&gt;
&lt;/p&gt;</description>
      <category>프로그래밍/코드카타</category>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/100</guid>
      <comments>https://hanong8.tistory.com/100#entry100comment</comments>
      <pubDate>Wed, 6 May 2026 21:19:13 +0900</pubDate>
    </item>
    <item>
      <title>[C++] 입출력 데이터 다루기 - 숫자 연산과 문자열 스트림 정리</title>
      <link>https://hanong8.tistory.com/98</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 이번 챕터에서 배운 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩 테스트 문제를 풀다 보면 입력값을 파싱하고 변환하는 데 생각보다 많은 시간을 쓰게 된다. 이번 강의에서는 C++에서 자주 쓰이는 숫자 처리 함수 3가지와, 문자열을 스트림처럼 다루는 &lt;code&gt;stringstream&lt;/code&gt; 활용법을 집중적으로 다뤘다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 수학 함수(&lt;code&gt;round&lt;/code&gt;, &lt;code&gt;ceil&lt;/code&gt;, &lt;code&gt;floor&lt;/code&gt;)는 &lt;code&gt;#include &amp;lt;cmath&amp;gt;&lt;/code&gt; 헤더가 필요하고, 문자열 스트림 관련 기능은 &lt;code&gt;#include &amp;lt;sstream&amp;gt;&lt;/code&gt;이 필요하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 숫자 다루기 &amp;mdash; cmath 함수 3종&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;양수 예&lt;/th&gt;
&lt;th&gt;음수 예&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;round(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;반올림 (0.5는 0에서 먼 쪽)&lt;/td&gt;
&lt;td&gt;round(3.5) &amp;rarr; 4&lt;/td&gt;
&lt;td&gt;round(-2.5) &amp;rarr; -3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ceil(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;올림&lt;/td&gt;
&lt;td&gt;ceil(3.2) &amp;rarr; 4&lt;/td&gt;
&lt;td&gt;ceil(-3.2) &amp;rarr; -3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;floor(x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;내림&lt;/td&gt;
&lt;td&gt;floor(3.7) &amp;rarr; 3&lt;/td&gt;
&lt;td&gt;floor(-3.7) &amp;rarr; -4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; &lt;code&gt;floor(-3.7)&lt;/code&gt;은 단순히 소수점을 버린 &lt;code&gt;-3&lt;/code&gt;이 아니라 &lt;code&gt;-4&lt;/code&gt;다. 음수에서 내림은 숫자가 더 작아지는 방향(더 음수)으로 간다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. stringstream &amp;mdash; 문자열을 스트림처럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;stringstream&lt;/code&gt;은 문자열을 마치 &lt;code&gt;cin&lt;/code&gt;처럼 다룰 수 있게 해주는 클래스다. 이미 갖고 있는 문자열에서 특정 타입의 값을 순서대로 추출하거나, 반대로 값을 문자열로 변환할 때 매우 유용하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공백 기준 분리&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;string str = &quot;123 X 67&quot;;
stringstream stream(str);
int num; char c; float f;
stream &amp;gt;&amp;gt; num &amp;gt;&amp;gt; c &amp;gt;&amp;gt; f;
// num=123, c='X', f=67.0&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 특정 문자 기준 분리 &amp;mdash; getline&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공백 외에 쉼표나 슬래시 같은 특정 문자로 구분된 문자열을 분리할 때는 &lt;code&gt;getline(스트림, 버퍼, 구분자)&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;string text = &quot;apple,banana,grape,orange&quot;;
stringstream ss(text);
string token;
while (getline(ss, token, ',')) {
    cout &amp;lt;&amp;lt; token &amp;lt;&amp;lt; endl;
}
// apple / banana / grape / orange&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패턴 정리:&lt;/b&gt; &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;는 공백 기준 분리, &lt;code&gt;getline(ss, buf, 구분자)&lt;/code&gt;는 특정 문자 기준 분리.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 진법 변환 &amp;mdash; hex 조작자 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10진수 &amp;rarr; 16진수&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int n = 4095;
stringstream ss;
ss &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; n;
string h = ss.str(); // &quot;fff&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;16진수 &amp;rarr; 10진수&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;string h = &quot;ff&quot;;
stringstream ss(h);
int n;
ss &amp;gt;&amp;gt; hex &amp;gt;&amp;gt; n; // n = 255&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방향 기억법:&lt;/b&gt; 10&amp;rarr;16은 &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;(출력 방향으로 넣기), 16&amp;rarr;10은 &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;(추출 방향으로 읽기).&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 오늘의 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 입력값 파싱은 &lt;code&gt;cin&lt;/code&gt;만 쓰거나 손으로 처리해왔는데, &lt;code&gt;stringstream&lt;/code&gt; 하나로 타입 변환과 구분자 분리가 깔끔하게 해결된다는 게 인상적이었다. 특히 16진수 변환 시 &lt;code&gt;hex&lt;/code&gt; 조작자를 쓰는 방식은 외워두면 실전에서 시간을 많이 아낄 수 있을 것 같다. 음수에서 &lt;code&gt;floor&lt;/code&gt;와 단순 소수점 버리기의 차이는 헷갈리기 쉬운 부분이므로 따로 정리해뒀다.&lt;/p&gt;</description>
      <category>프로그래밍/코드카타</category>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/98</guid>
      <comments>https://hanong8.tistory.com/98#entry98comment</comments>
      <pubDate>Tue, 28 Apr 2026 21:09:02 +0900</pubDate>
    </item>
    <item>
      <title>[코딩 테스트] 어떻게 준비하고 어떻게 풀어야 할까?</title>
      <link>https://hanong8.tistory.com/97</link>
      <description>&lt;h2&gt;1. 코딩 테스트란 무엇인가&lt;/h2&gt;
&lt;p&gt;코딩 테스트는 제한된 시간 안에 프로그래밍 언어로 주어진 문제를 풀어내는 시험이다. 많은 IT 기업의 채용 프로세스에 포함되어 있으며, 단순히 정답을 맞히는 것을 넘어 문제에서 요구하는 성능을 충족하는 코드를 구현하는 것이 핵심이다.&lt;/p&gt;
&lt;p&gt;흔히 &amp;quot;머리가 좋아야 풀 수 있다&amp;quot;고 생각하지만, 코딩 테스트는 IQ 테스트가 아니다. 시험에서 요구하는 사항을 명확히 인지하고 반복 트레이닝한다면 충분히 통과할 수 있는 시험이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;프로그래머스 기준으로 취업을 목표로 한다면 레벨 2 ~ 3 수준을 목표로 삼는 것이 적절하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;2. 나의 현재 수준 파악하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;레벨 0~1&lt;/strong&gt;: 기본 문법은 숙지했으나 자료구조·알고리즘을 모름. 직관과 단순 논리로 문제 해결.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;레벨 1~2&lt;/strong&gt;: 자료구조·알고리즘을 배우는 중. 간단한 자료구조를 적용해서 문제를 풀 수 있음.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;레벨 2~3&lt;/strong&gt;: 다양한 접근 방식으로 풀이 가능. 요구 성능 파악 및 이에 맞는 코드 구현 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 코딩 테스트에 필요한 4가지 역량&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;요구 사항 파악&lt;/strong&gt;: 문제를 읽고 핵심을 요약하고, 제약 사항을 정확히 이해한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;효율적인 코드 구현&lt;/strong&gt;: 다량의 입력값을 처리할 수 있는 성능 충족 코드를 작성한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;논리적 설계 및 구현&lt;/strong&gt;: 의사 코드를 먼저 작성하고, 이를 실제 코드로 옮긴다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예외 처리 능력&lt;/strong&gt;: 사전에 예외 케이스를 발굴하고 테스트 케이스에 반영한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;4. 문제를 푸는 전략 — 70:30 법칙&lt;/h2&gt;
&lt;p&gt;풀이 시간의 70%는 분석에, 30%는 구현에 써야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;305&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2nxrA/dJMcag6nNfX/HNXTkD9pAO4eZZs90ePYwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2nxrA/dJMcag6nNfX/HNXTkD9pAO4eZZs90ePYwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2nxrA/dJMcag6nNfX/HNXTkD9pAO4eZZs90ePYwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2nxrA%2FdJMcag6nNfX%2FHNXTkD9pAO4eZZs90ePYwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;305&quot; height=&quot;347&quot; data-origin-width=&quot;305&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 프로그래머스 채점 방식 이해하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;정확성 테스트&lt;/strong&gt;: 코드가 리턴한 값과 실제 정답이 일치하는지 확인. 10초 내 리턴 필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;효율성 테스트&lt;/strong&gt;: 구현한 코드가 요구 성능을 충족하는지 확인. 출제자 정답 코드의 평균 수행 시간이 기준.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;오픈채팅방 예시 문제에서 정확성은 통과했지만 효율성에서 시간 초과가 발생했다. 원인은 O(N²) 탐색 구조였으며, HashMap으로 아이디-닉네임을 별도 관리하는 방식으로 해결할 수 있었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 효율적인 학습법 — 데이터 기반 목표 세우기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;(X) &amp;quot;한 달에 100문제 풀기&amp;quot;&lt;/li&gt;
&lt;li&gt;(O) &amp;quot;오답 유형 분류 후, 취약 분야 60문제 + 복습 40문제&amp;quot; — 한 사이클이 끝나면 다시 데이터를 보고 동적으로 조정.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;일주일에 한 번은 실전처럼 타이머를 켜고 문제를 풀어보는 습관도 중요하다. 모르는 문제가 나왔을 때는 약 60분 정도 고민한 뒤 풀이를 확인하고, 풀지 못한 이유와 정답 코드의 전략을 정리해두는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 오늘의 회고&lt;/h2&gt;
&lt;p&gt;오늘 가장 인상 깊었던 부분은 문제를 푸는 과정 자체를 체계화하는 것의 중요성이었다. 구현보다 분석에 더 많은 시간을 써야 한다는 것, 그리고 시간 초과가 발생했을 때 코드가 아니라 알고리즘의 구조를 먼저 의심해야 한다는 것을 예시 문제를 통해 직접 경험했다. &lt;/p&gt;</description>
      <category>프로그래밍/코드카타</category>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/97</guid>
      <comments>https://hanong8.tistory.com/97#entry97comment</comments>
      <pubDate>Mon, 27 Apr 2026 20:12:21 +0900</pubDate>
    </item>
    <item>
      <title>[UnrealEngine] 멀티플레이어 디펜스 `SagoMagic` 프로젝트 KPT 회고</title>
      <link>https://hanong8.tistory.com/96</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;SagoMagic.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rjGRP/dJMcahD99V3/KkNBEpZlXSpR3HR11KfTt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rjGRP/dJMcahD99V3/KkNBEpZlXSpR3HR11KfTt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rjGRP/dJMcahD99V3/KkNBEpZlXSpR3HR11KfTt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrjGRP%2FdJMcahD99V3%2FKkNBEpZlXSpR3HR11KfTt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;687&quot; height=&quot;458&quot; data-filename=&quot;SagoMagic.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;프로젝트 개요&lt;/span&gt;&lt;/h2&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-tableconvert-icon=&quot;true&quot; data-tableconvert-processed=&quot;true&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;프로젝트명&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;SagoMagic (Co-op 탑다운 타워디펜스)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;엔진&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;Unreal Engine 5.6, C++ / Blueprint&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;네트워크&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;Dedicated Server, Seamless Travel&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;핵심 기술&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;GAS, GMS, 그리드 건축, 격자형 인벤토리, Fragment 패턴&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;팀 규모&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;7인 (&amp;nbsp;곽은서, 김현, 박원종, 이준로, 임영택, 주철민, 허태린&amp;nbsp;)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;개발 기간&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;4 / 1 ~ 4 / 24&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;게임 루프는 로비 &amp;rarr; 정비 단계(BuildState) &amp;rarr; 웨이브 전투(CombatState) &amp;rarr; 결과 화면(ResultState)의 사이클을 반복하며 베이스캠프(ASMBaseCampActor)의 HP가 0이 되면 패배, 마지막 웨이브를 클리어하면 승리한다.4인 Dedicated Server 구조로 운영되며 맵 전환은 Seamless Travel(L_Transition 경유)로 처리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;Keep - 잘 된 것, 유지할 것&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;1. GAS 기반 전투&amp;middot;건축 시스템의 통합 설계&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB0aaj/dJMcafsO9TF/avp7GF1xnTpxxg4I8wkAKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB0aaj/dJMcafsO9TF/avp7GF1xnTpxxg4I8wkAKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB0aaj/dJMcafsO9TF/avp7GF1xnTpxxg4I8wkAKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB0aaj%2FdJMcafsO9TF%2Favp7GF1xnTpxxg4I8wkAKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;291&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;이번 프로젝트에서 가장 큰 기술적 성과는 GAS를 단순히 &quot;스킬 시스템&quot;으로 국한하지 않고, 전투&amp;middot;건축&amp;middot;쿨타임&amp;middot;상태 관리를 아우르는 프로젝트 전체의 공통 인프라로 정착시킨 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;어빌리티 시스템 컴포넌트(SMAbilitySystemComponent)는 플레이어, 몬스터, 건물, 베이스캠프 각각에 독립적인 AttributeSet을 붙여 관리한다. USMPlayerAttributeSet은 플레이어의 HP와 이동 속도를, USMMonsterAttributeSet은 몬스터 스탯을, USMBuildingAttributeSet은 건물 내구도를, USMBaseCampAttributeSet은 베이스캠프 HP를 전담한다. 이처럼 어트리뷰트를 객체 타입별로 분리함으로써 각 객체의 상태 변화가 다른 객체에 간섭하지 않으며, GE(GameplayEffect)의 적용 대상을 태그 기반으로 명확히 구분할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;스킬 실행 흐름은 다음과 같이 정형화되어 있다. 마우스 클릭이 TryActivateAbilitiesByTag()를 호출하면GA_SkillBase::ActivateAbility()가 실행되고, 먼저 인벤토리에서 FSMCompiledSkillSummary를 로드해 최종 수치(피해량, 쿨타임, 범위, 지속시간, 업그레이드 태그)를 확보한다. 이후 CommitAbility()로 GE_SkillCooldown을 적용하고, 몽타주를 재생하여 AN_SendEvent AnimNotify가 발동되는 타이밍에 GameplayEvent를 전달한다. 클라이언트는 이 시점에 마우스 좌표를 수집해 ServerSetReplicatedTargetData로 서버에 전송하며, 서버는 OnTargetDataReadyCallback()에서 실제 피해를 적용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;건물 배치(GA_BuildPlace) 역시 동일한 어빌리티 파이프라인으로 처리된다. ServerRPC_RequestPlaceBuilding() 호출 후 GA_BuildPlace::ActivateAbility()가 서버에서만 실행되어 셀 점유 검증 &amp;rarr; GE로 골드 차감 &amp;rarr; 건물 스폰 + GridData 등록 순서로 원자적으로 처리된다. 스킬 발동과 건물 배치가 동일한 어빌리티 인터페이스를 공유하기 때문에, 어빌리티 등록/해제, 쿨타임 처리, 태그 기반 발동 차단 등의 공통 로직을 중복 없이 재사용할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;쿨타임 관리는 GE_SkillCooldown(HasDuration)과 SetByCaller(Data.Cooldown) 조합으로 동적 주입 방식을 채택했다. GA의 ActivationBlockedTags에 Cooldown.Skill.* 태그를 등록해두면, 해당 GE가 활성화된 동안에는 어빌리티 발동이 자동으로 차단된다. GE의 GrantedTags가 부여/제거되는 시점에 USMSkillCooldownWidget이 실시간으로 갱신되어, 별도의 UI 타이머 로직 없이도 정확한 쿨타임 표시가 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;2. GMS 기반 UI 디커플링의 구조적 효과&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;멀티플레이어 게임에서 UI 갱신을 서버 &amp;rarr; 클라이언트 복제에 의존하면 불필요한 네트워크 트래픽이 발생하고, 게임 로직과 UI 간의 결합도가 높아진다. 이를 해결하기 위해 GameplayMessageRouter 플러그인 기반의 GameplayMessageSubsystem(GMS)을 UI 갱신의 단일 채널로 채택했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;구체적인 채널 구조는 다음과 같다. 웨이브 진행 정보는 UI.Event.Wave 채널로 방송되어 USMWaveTimeWidget이 수신하고, 베이스캠프 HP 변화는 USMBaseCampAttributeSet::OnRep_Health()에서 UI.Event.BaseCamp로 방송되어 USMBaseCampHPBarWidget이 수신한다. 알림 메시지는 ClientRPC_ShowNotification()에서 UI.Event.Notification으로 방송되고, 건축 모드 진입/해제는 UI.Event.BuildMode, 편집 모드는 UI.Event.EditMode, 인벤토리 관련 조작은 SM.Message.Inventory.* 채널을 통해 처리된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;이 구조의 핵심 이점은 게임 로직 클래스가 어떤 위젯도 직접 참조하지 않는다는 점이다. 예를 들어 ASMBaseCampActor는 HP가 변할 때 GMS에 메시지를 방송하는 것 외에 UI에 대해 아무것도 알지 못한다. 위젯을 교체하거나 추가하더라도 로직 코드를 건드릴 필요가 없다. 실제 개발 과정에서 HUD 구조가 여러 차례 변경되었음에도 게임 로직 측에서의 수정이 전혀 필요 없었던 것이 이를 잘 보여준다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;3. Fragment 패턴 기반 데이터 주도 아키텍처&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAAnKb/dJMcag6lUgW/vQiRchkJJU3iaYbb7k34m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAAnKb/dJMcag6lUgW/vQiRchkJJU3iaYbb7k34m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAAnKb/dJMcag6lUgW/vQiRchkJJU3iaYbb7k34m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAAnKb%2FdJMcag6lUgW%2FvQiRchkJJU3iaYbb7k34m1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;484&quot; height=&quot;455&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;535&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;인벤토리와 스킬 시스템의 데이터 설계에서 Fragment 패턴을 채택한 것은 이번 프로젝트의 아키텍처적 하이라이트 중 하나다. USMItemDefinition이 필요한 Fragment를 조합하는 방식으로, 아이템 종류마다 클래스를 새로 만들 필요 없이 Fragment 구성만으로 다양한 아이템을 정의할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;각 Fragment의 역할은 명확하게 분리되어 있다. USMGridShapeFragment는 비트마스크(FSMGridMaskData)로 격자 점유 형태를 정의하며 0&amp;deg;/90&amp;deg;/180&amp;deg;/270&amp;deg; 회전을 지원하고, 배치 충돌 감지도 비트마스크 AND 연산으로 처리되어 루프 기반 충돌 검사 대비 성능이 우수하다. USMAbilityFragment는 아이템과 어빌리티 클래스를 연결하며 관련 태그를 보유한다. USMGemModifierFragment는 젬 효과의 종류&amp;middot;수치&amp;middot;장착 조건 태그(RequiredAllTargetTags, RequiredAnyTargetTags, BlockedTargetTags)를 포함해 태그 기반 호환성 검사를 가능하게 한다. USMInternalInventoryFragment와 USMSkillProgressionFragment는 스킬 아이템의 내부 인벤토리 구조와 레벨업 규칙을 담당한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;스킬 발동 시마다 젬 효과를 재순회하는 대신 FSMCompiledSkillSummary에 수치를 미리 캐싱해두는 설계도 중요한 최적화다. BuildSkillSummary()는 DT_Skill의 베이스 수치에 레벨 증가량을 합산하고, 내부 젬을 순회하며 FinalDamage *= (1 + %), FinalCooldown *= (1 - %) 등의 승산 합성을 적용한 뒤, GrantedBehaviorTags를 BehaviorTags에 병합하여 동작 분기 플래그로 활용한다. 이 캐시는 인벤토리 구성이 바뀔 때만 재계산되므로, 매 어빌리티 발동마다 순회 비용이 발생하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;4. 그리드 건축 시스템의 완성도&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5y1SH/dJMcahD99VZ/n2b1owp9KbVlS37IGjUdPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5y1SH/dJMcahD99VZ/n2b1owp9KbVlS37IGjUdPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5y1SH/dJMcahD99VZ/n2b1owp9KbVlS37IGjUdPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5y1SH%2FdJMcahD99VZ%2Fn2b1owp9KbVlS37IGjUdPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;618&quot; height=&quot;334&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;ASMGridManager는 1D 배열(Index = Y &amp;times; Width + X) 기반으로 전체 그리드 상태를 관리한다. 각 셀은 bIsOccupied, BuildingType, OwnerId를 클라이언트에 DOREPLIFETIME으로 복제하고, PlacedActor는 서버 전용으로 유지한다. 이는 건물 액터 자체가 bReplicates = true로 별도 복제되기 때문에 이중 복제를 피하면서도 클라이언트가 그리드 상태를 정확히 인식할 수 있게 하는 효율적인 설계다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;BakeGridHeights()는 StartPlay 시 1회 Landscape의 Z값을 사전 베이크하여, 건물 배치 시마다 지형 높이를 실시간으로 계산하는 비용을 제거한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;경로 기반 펜스 배치에서의 A* 구현은 방향 전환 패널티를 포함하여 직선 경로를 우선시하도록 설계했다. 이를 통해 플레이어가 두 점을 찍으면 꺾이는 경로보다 직선에 가까운 자연스러운 펜스 배치가 이루어진다. 경로의 방향 전환 지점은 GetEffectiveCornerInfo()로 자동 감지되어 ConvertToCornerPreview()를 통해 코너 메시로 교체되고 적절한 Yaw가 계산된다. 경로 끝점에서도 인접 점유 셀을 검사해 접합부 코너를 자동 처리하므로 플레이어가 코너를 수동으로 배치할 필요가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;편집 모드(SMEditModeComponent)에서 건물 이동 시 클라이언트는 로컬에서 선제적으로 이동을 표시하고, ServerRPC_PreviewMove &amp;rarr; MulticastRPC_PreviewMove로 다른 클라이언트에 동기화한다. 이동 확정 시 ServerRPC_MoveBuildings가 원자적으로 검증하여 전체 성공 또는 전체 원위치 처리(all-or-nothing)를 보장한다. 이동 중 Pawn 충돌 채널을 MulticastRPC_SetBuildCollision으로 임시 비활성화해 플레이어가 이동 중인 건물에 막히지 않도록 처리한 것도 세심한 UX 고려였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;5. 웨이브 시스템의 서버-클라이언트 동기화 설계&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;웨이브 전환 시 가장 까다로운 문제는 서버와 모든 클라이언트의 DataAsset 로드가 완료된 시점에만 전투를 시작해야 한다는 것이었다. 이를 위해 BuildState::Enter() 시점에 USMWaveManagerSubsystem::PreSpawnForWave()가 DT_Monster에서 MonsterDataAsset 경로를 수집하고, AssetManager로 PrimaryAssetId를 조회한 뒤 GameState::SetAssetsToLoad()를 통해 클라이언트에 로드 목록을 복제한다. 서버는 LoadAssetsByIDWithBundles({&quot;Server&quot;}), 클라이언트는 OnRep_AssetsToLoad() 콜백에서 LoadAssetsByIDWithBundles({&quot;Client&quot;})를 비동기로 실행하며, 각 클라이언트가 로드 완료 시 ServerNotifyClientLoadComplete()를 호출해 ReadyClientCount를 증가시킨다. 전원이 준비되면 OnReadyForCombat이 발동되어 CombatState로 전환된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;CombatState에서 몬스터는 SpawnInterval 간격으로 순차 활성화된다. PreSpawn 단계에서 이미 Hidden 상태로 스폰해 두었기 때문에 전투 시작 시점의 스폰 부하가 없으며, 시각적으로도 몬스터가 갑자기 대량으로 나타나는 현상을 방지할 수 있다. 타임오버 패널티로 잔존 몬스터가 SelfKill하여 베이스캠프에 고정 데미지를 입히는 메커니즘은, 플레이어가 웨이브를 질질 끌어 시간을 낭비하는 전략을 방지하는 게임 디자인적 의도와 서버 측 리소스 정리를 동시에 달성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;6. 네트워크 보안 및 서버 권한 설계&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;보안 설계에서 세 가지 핵심 원칙을 일관되게 유지했다. 첫째, 이속 핵 방어를 위해 서버가 1초마다 MaxWalkSpeed를 검증하고 허용 오차(110%) 초과 시 강제 복구한다. 둘째, 건축 배치는 클라이언트의 고스트 프리뷰가 순수 로컬 표현에 그치고 실제 배치는 GA_BuildPlace [ServerOnly]에서 이중 검증 후 처리된다. 셋째, 인벤토리의 모든 Add/Move/Drop 조작이 서버에서 Authority 체크를 통과해야만 반영된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;USMSyncDataManager를 서버 전용으로 제한한 것도 의도적인 설계 제약이다. 클라이언트에서 DataTable 원본에 직접 접근할 수 없으므로 치트 도구를 통한 스탯 조작 시도를 구조적으로 차단한다. 스킬 어빌리티를 ASMPlayerCharacter의 ASC가 아닌 ASMPlayerState의 ASC에 등록한 것도 중요한 설계 결정이다. 캐릭터가 리스폰으로 재생성되더라도 PlayerState는 유지되기 때문에, 부활 후에도 어빌리티 등록 상태와 쿨타임 정보가 그대로 보존된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;Problem - 아쉬웠던 점, 겪었던 문제&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;1. GAS 초기 설계 비용과 구조 재설계&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zEzcR/dJMcaad0yGl/RVAn2eeDXbZR70HeTW2NM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zEzcR/dJMcaad0yGl/RVAn2eeDXbZR70HeTW2NM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zEzcR/dJMcaad0yGl/RVAn2eeDXbZR70HeTW2NM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzEzcR%2FdJMcaad0yGl%2FRVAn2eeDXbZR70HeTW2NM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;473&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;607&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;GAS는 초기 아키텍처 결정이 이후 전체 개발 흐름에 미치는 영향이 매우 크다. 이번 프로젝트에서도 AttributeSet 계층 구조와 GameplayEffect 연산 파이프라인을 초반에 확정하는 데 예상보다 많은 시간이 소요되었고, 프로젝트 중반에 일부 Effect 구조를 재설계하는 일이 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;특히 피해 계산 흐름에서 GE_InstantDamage의 Execution을 통한 계산과 SetByCaller를 통한 직접 수치 주입 방식 사이의 선택을 두고 초반 설계가 흔들렸다. 최종적으로 SetByCaller(Data.Damage.Amount) 방식을 채택했지만, 이 결정이 늦어지면서 일부 어빌리티에서 두 방식이 혼재하는 시기가 있었고 이를 정리하는 데 추가 공수가 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;ActivationBlockedTags와 ActivationRequiredTags의 설계도 초반에는 충분히 체계화되지 않아, 전투 중 건축 모드 진입 차단(CombatState에서 State.Build.Place 태그 차단) 같은 요구사항이 생길 때마다 태그 구조를 추가하는 방식으로 대응했다. 처음부터 게임 상태 전환과 연동되는 태그 계층을 설계 문서로 정리해두었다면 이런 추가 작업을 줄일 수 있었을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;2. 멀티플레이어 복제 및 RPC 부하&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EB3oB/dJMcafsO9TD/cAKrutpFSWAI1tnykwoBd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EB3oB/dJMcafsO9TD/cAKrutpFSWAI1tnykwoBd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EB3oB/dJMcafsO9TD/cAKrutpFSWAI1tnykwoBd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEB3oB%2FdJMcafsO9TD%2FcAKrutpFSWAI1tnykwoBd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;744&quot; height=&quot;264&quot; data-origin-width=&quot;1110&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;Dedicated Server 환경에서 액터 수가 늘어남에 따라 복제 부하가 누적되는 문제가 있었다. 웨이브 후반부에 ASMMonsterBase 객체가 다수 활성화된 상태에서 ASMMonsterProjectile 스폰, ASMBaseCampActor HP 변화, ASMGridManager 상태 갱신이 동시에 발생하면 클라이언트 측에서 리플리케이션 지연이 체감될 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;몬스터 ASC를 Minimal 복제 모드로 설정한 것은 클라이언트 태그 동기화만을 위한 최소한의 복제라는 의도였으나, 실제로는 UGA_MonsterAttackBase 실행 시 GameplayCue의 네트워크 발동이 예상보다 많은 트래픽을 발생시켰다. 원거리 몬스터(Squid)의 투사체는 서버에서S 스폰되어 bReplicates = true로 모든 클라이언트에 복제되는데, 웨이브 후반에 다수의 투사체가 동시에 활성화되면 복제 대역폭 소비가 컸다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;SMEditModeComponent의 건물 이동 미리보기에서 ServerRPC_PreviewMove &amp;rarr; MulticastRPC_PreviewMove 체인이 건물 수만큼 반복 호출될 수 있어, 다수의 건물을 동시에 이동할 때 RPC 호출 빈도가 높아지는 문제도 있었다. 이 부분은 건물 이동 데이터를 배열로 묶어 단일 RPC 호출로 처리하도록 배칭(batching)하지 못한 것이 아쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;3. 몬스터 AI 연산 부하&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAULjF/dJMcafsO9TC/4asflrZVDRA2JKkKUVLkAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAULjF/dJMcafsO9TC/4asflrZVDRA2JKkKUVLkAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAULjF/dJMcafsO9TC/4asflrZVDRA2JKkKUVLkAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAULjF%2FdJMcafsO9TC%2F4asflrZVDRA2JKkKUVLkAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;735&quot; height=&quot;335&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;ASMMonsterAIController는 BehaviorTree와 타이머 기반 UpdateTargetAndTryAttack()을 함께 운영한다. 웨이브 후반에 수십 개의 몬스터가 동시에 활성화되면 각 AIController의 타이머가 거의 같은 프레임에 집중될 수 있으며, 여기에 FindBuildingInRange(), FindNearestPlayerInRange() 같은 오버랩 탐색이 겹치면 CPU 스파이크가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;ASMGridManager::FindPath()의 A* 연산도 그리드 크기와 건물 배치 밀도에 따라 비용이 증가한다. 현재 구현에서는 몬스터의 이동 목표(TargetActor)가 항상 가장 가까운 베이스캠프로 고정되어 있고, 장애물(건물)이 변경될 때마다 경로를 재계산하는 구조인데, 이 재계산이 전투 중에도 빈번하게 발생할 수 있었다. 특히 편집 모드에서 건물 이동이 일어날 때 다수의 몬스터가 동시에 경로를 재계산하는 구간이 문제였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;USMWaveManagerSubsystem의 틱 처리도 최적화가 부족했다. 서브시스템이 매 틱마다 웨이브 상태를 검사하는 대신, 이벤트 기반으로 전환하거나 틱 간격을 늘렸다면 불필요한 연산을 줄일 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;4. GAS Prediction 구현의 미완성&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;현재 구현에서 스킬 발동 시 클라이언트는 GameplayCue의 코스메틱 이펙트만 선예측하고, 실제 피해 처리는 서버에서 OnTargetDataReadyCallback() 이후에 이루어진다. 이 구조는 서버 권한을 보장한다는 측면에서 올바르지만, 네트워크 레이턴시가 높을 때 스킬 이펙트와 실제 피해 판정 사이의 시각적 불일치가 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;건축 배치는 ServerOnly 어빌리티로 처리되어 클라이언트 예측이 전혀 없다. 클라이언트가 배치 요청을 보낸 후 서버 응답을 받기까지 건물이 나타나지 않는 딜레이가 존재하며, 이것이 빠른 연속 배치 시에는 체감될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;FPredictionKey 기반의 예측 롤백을 건축과 인벤토리 조작에까지 확장하지 못한 것이 UX 측면에서 아쉬운 부분으로 남았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;5. 인벤토리 복제와 클라이언트 반응성&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLgo0G/dJMcag6lUgX/4UM06vI1dH83fwVpgb7iH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLgo0G/dJMcag6lUgX/4UM06vI1dH83fwVpgb7iH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLgo0G/dJMcag6lUgX/4UM06vI1dH83fwVpgb7iH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLgo0G%2FdJMcag6lUgX%2F4UM06vI1dH83fwVpgb7iH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;761&quot; height=&quot;247&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;USMInventoryComponent는 COND_OwnerOnly로 복제되어 보안과 트래픽 측면에서는 이상적이지만, 인벤토리 조작의 모든 경로가 서버를 경유해야 한다는 제약이 생긴다. 아이템 이동, 젬 장착, 스킬 레벨업 등 인벤토리 내부 조작 시 서버 왕복 레이턴시만큼 반응이 늦게 느껴질 수 있으며, 이를 보완하는 로컬 예측 레이어가 구현되지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;비트마스크 기반 충돌 감지는 성능 측면에서는 효율적이지만, FSMGridMaskData의 회전 변환 로직이 복잡해 일부 엣지 케이스(아이템 회전 후 격자 경계 근처 배치)에서 잘못된 충돌 판정이 발생하는 버그가 간헐적으로 재현되었다. 이 문제는 회전 변환 행렬 계산의 부호 처리 오류에서 기인했으나, 프로젝트 기간 내에 완전히 수정하지 못했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;Try - 앞으로 시도할 것, 개선 방향&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;1. 대규모 환경 최적화 체계 도입&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;다음 장기 프로젝트에서는 Unreal Engine 5의 대규모 최적화 도구를 본격적으로 활용할 계획이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;Significance Manager를 도입하여 카메라 거리, 화면 점유율, 플레이어와의 상호작용 빈도에 따라 몬스터 AI 틱 빈도를 동적으로 조절한다. 예를 들어 화면 밖 원거리 몬스터는 Significance 값이 낮게 설정되어 AI 틱이 0.3~0.5초 간격으로 줄어들고, 플레이어 근처의 몬스터는 정상 빈도로 유지된다. 이 접근만으로도 웨이브 후반 AI 연산 부하의 상당 부분을 줄일 수 있을 것으로 기대한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;A* 경로 계산은 온디맨드 캐싱 전략으로 전환한다. 전투 중에는 그리드 변화(건물 배치/삭제/이동)가 발생할 때만 영향 받는 몬스터 그룹의 경로를 재계산하고, 그 외에는 기존 경로를 재사용한다. 이를 위해 그리드 셀 변경 이벤트를 발행하고, 해당 셀을 경로의 일부로 포함하는 몬스터만 선택적으로 재계산하는 구조가 필요하다. ASMGridManager에 구독 패턴을 추가하면 구현 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;USMWaveManagerSubsystem의 상태 검사는 틱 기반에서 이벤트 기반으로 전환한다. 몬스터가 사망할 때마다 CheckWaveCleared()를 호출하는 현재 방식은 이미 이벤트 기반에 가깝지만, 타임오버 처리 등 일부 흐름이 틱에 의존하고 있어 이를 타이머 델리게이트로 대체할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;렌더링 측면에서는 World Partition과 HLOD를 도입해 대규모 맵에서 원거리 오브젝트의 렌더링 비용을 줄이고, Occlusion Culling과 Distance Culling을 병행하여 화면에 보이지 않는 건물과 몬스터의 렌더링 스레드 부하를 제거한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;2. GAS Prediction 고도화&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;FPredictionKey 기반의 클라이언트 예측을 스킬 피해 판정을 넘어 건축 배치까지 확장한다. 건물 배치의 경우, 클라이언트가 서버 검증 전에 고스트 프리뷰가 아닌 실제 건물 메시를 로컬에서 즉시 표시하고, 서버 검증 실패 시 롤백하는 방식으로 반응성을 개선할 수 있다. 이를 위해 GA_BuildPlace에 LocalPredicted 활성화 정책을 적용하고, 서버 검증 실패 시 OnAbilityCancelled 콜백에서 클라이언트 측 임시 건물 액터를 제거하는 롤백 로직을 구현해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;인벤토리 조작의 경우 서버 왕복 없이 로컬에서 선예측 상태를 표시하고, 서버 응답이 오면 결과를 확정하거나 원상 복귀하는 Optimistic UI 패턴을 도입한다. GAS의 LocalPredicted 정책을 인벤토리 어빌리티에도 적용하거나, 별도의 클라이언트 측 임시 상태 레이어를 두는 방식으로 구현할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;몬스터 투사체의 경우, 서버에서만 스폰하고 복제하는 현재 방식 대신 클라이언트에서 발사 시점을 예측하여 로컬 코스메틱 투사체를 생성하고, 피해 판정은 서버에서만 처리하는 방식으로 체감 반응성을 높인다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;3. 복제 전략 세분화&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;ASMGridManager의 GridData 배열은 현재 DOREPLIFETIME 단일 조건으로 전체가 복제된다. 실제로는 변경된 셀만 클라이언트에 전달하면 충분한데, 배열 전체를 복제하면 웨이브 시작 시처럼 다수의 셀이 동시에 변경되는 구간에서 불필요한 트래픽이 발생한다. DOREPLIFETIME_CONDITION과 ReplicatedUsing 콜백을 조합하거나, FastArraySerializer를 활용해 변경된 요소만 델타 전송하도록 개선할 계획이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;몬스터 투사체의 복제도 재검토가 필요하다. 수명이 짧고 수가 많은 투사체 액터를 전통적인 액터 복제로 처리하는 것은 연결/해제 오버헤드가 크다. UReplicationGraph의 UReplicationGraphNode_ActorList를 활용하거나, 투사체 상태를 별도의 경량 구조체 배열로 압축해 GameState를 통해 복제하는 커스텀 복제 전략이 더 효율적일 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;RPC 배칭도 개선 과제다. 편집 모드의 다중 건물 이동처럼 여러 엔티티의 상태를 동시에 변경하는 작업은 단일 RPC에 배열을 담아 전송하도록 통일한다. ServerRPC_MoveBuildings는 이미 이 방식을 사용하고 있으나, 미리보기 단계의 ServerRPC_PreviewMove도 배칭으로 통일해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;4. 프로파일링 기반 개발 문화 정착&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;이번 프로젝트에서 성능 문제는 주로 증상이 나타난 후 사후 대응 방식으로 처리했다. 다음 프로젝트에서는 Unreal Insights와 stat unit, stat game, stat net 명령을 개발 초기부터 주기적으로 모니터링하여 병목을 선제적으로 발견하는 습관을 팀 전체에 정착시킨다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;특히 네트워크 프로파일링에서 NetworkProfiler를 활용해 복제 대역폭을 액터 단위로 분석하고, RPC 호출 빈도와 페이로드 크기를 정량적으로 파악하는 과정을 스프린트마다 반복한다. 이를 통해 주관적인 &quot;느린 것 같다&quot;는 판단 대신 수치 기반으로 최적화 우선순위를 결정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;GAS 측면에서는 AbilitySystem.DebugAbilities 콘솔 명령과 ShowDebug AbilitySystem 활성화를 통해 어트리뷰트 수치, 활성 Effect, 태그 상태를 실시간으로 확인하는 디버깅 워크플로를 팀 표준으로 수립한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;5. 게임 기획과 기술의 통합적 시각 확보&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cleSwy/dJMcajooe96/3ldZkXJKA9GqO8iwN70LSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cleSwy/dJMcajooe96/3ldZkXJKA9GqO8iwN70LSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cleSwy/dJMcajooe96/3ldZkXJKA9GqO8iwN70LSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcleSwy%2FdJMcajooe96%2F3ldZkXJKA9GqO8iwN70LSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;303&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;Fragment 패턴과 DataTable 기반의 데이터 주도 아키텍처를 구축해두었기 때문에, 다음 단계는 이 구조를 플레이테스트 피드백 사이클에 실제로 활용하는 것이다. 젬 조합별 스킬 성능 데이터를 로깅하고, 어떤 조합이 실제 플레이에서 지배적인지 분석하는 인게임 통계 시스템을 USMSyncDataManager 위에 구축할 계획이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;밸런스 조정을 위해 코드를 재컴파일할 필요 없이 DT_Skill, DT_Wave, DT_Building 수치를 런타임에 핫리로드하는 기능도 다음 프로젝트에서 구현한다. Unreal Engine의 FTableRowBase 기반 DataTable은 에디터에서 이미 핫리로드를 지원하므로, 이를 패키징된 빌드에서도 외부 JSON으로 오버라이드할 수 있는 레이어를 추가하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;궁극적으로는 기술 아키텍처가 게임 기획의 실험 속도를 뒷받침하는 구조, 즉 프로그래머의 개입 없이 기획자가 새로운 스킬 조합이나 웨이브 구성을 빠르게 시험하고 검증할 수 있는 파이프라인을 완성하는 것이 목표다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;633&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJVcs0/dJMcafsO9TI/ENRKfb55F9TzkwDRnFNNvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJVcs0/dJMcafsO9TI/ENRKfb55F9TzkwDRnFNNvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJVcs0/dJMcafsO9TI/ENRKfb55F9TzkwDRnFNNvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJVcs0%2FdJMcafsO9TI%2FENRKfb55F9TzkwDRnFNNvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;691&quot; height=&quot;417&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;633&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로그래밍/개발일지</category>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/96</guid>
      <comments>https://hanong8.tistory.com/96#entry96comment</comments>
      <pubDate>Fri, 24 Apr 2026 16:42:41 +0900</pubDate>
    </item>
    <item>
      <title>[Unreal Engine5] GA_SkillBase 리팩토링 회고 &amp;mdash; 애니메이션 추가와 GameplayCue owner 복제 이슈</title>
      <link>https://hanong8.tistory.com/95</link>
      <description>&lt;p&gt;프로토타입 단계에서 급히 쌓인 스킬 GA들에 애니메이션을 입히려다 보니, 각 GA가 같은 네트워크 보일러플레이트를 따로따로 들고 있고 그 안에 &lt;code&gt;LocalPredicted&lt;/code&gt; 특유의 함정까지 숨어 있었다. 이번 글에서는 &lt;strong&gt;(1) 왜 리팩토링이 필요했는지&lt;/strong&gt;, &lt;strong&gt;(2) 어떤 구조로 통합했는지&lt;/strong&gt;, 그리고 &lt;strong&gt;(3) 리팩토링 과정에서 드러난 &lt;code&gt;GameplayCue&lt;/code&gt; owner-skip 버그를 어떻게 해결했는지&lt;/strong&gt; 세 가지를 정리한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 리팩토링 전 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLAuZL/dJMcagL1J0t/rkN1Q4X73AaxuvGIC5KIr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLAuZL/dJMcagL1J0t/rkN1Q4X73AaxuvGIC5KIr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLAuZL/dJMcagL1J0t/rkN1Q4X73AaxuvGIC5KIr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLAuZL%2FdJMcagL1J0t%2FrkN1Q4X73AaxuvGIC5KIr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;582&quot; height=&quot;629&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;629&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;공통 문제 (보일러플레이트의 출처)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스킬마다 &amp;quot;로컬 마우스 좌표 → &lt;code&gt;ServerSetReplicatedTargetData&lt;/code&gt; → 서버 &lt;code&gt;AbilityTargetDataSetDelegate&lt;/code&gt; 바인딩 → &lt;code&gt;CallReplicatedTargetDataDelegatesIfSet&lt;/code&gt; → 콜백에서 종료&amp;quot; 루틴을 &lt;strong&gt;따로 재구현&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TryGetMouseGroundLocation&lt;/code&gt;이 &lt;code&gt;GA_SkillField&lt;/code&gt;에만 존재. 다른 스킬은 각자 &lt;code&gt;GetHitResultUnderCursor&lt;/code&gt; / 컨트롤러 회전으로 제각각 처리.&lt;/li&gt;
&lt;li&gt;몽타주/노티파이를 연결할 구조가 없어 애니메이션을 넣으려면 모든 파일에 동일 코드를 추가해야 함.&lt;/li&gt;
&lt;li&gt;클라 예측 Cue 호출이 GA_SkillField::OnSkillEffect처럼 GA 안쪽에서 일어남 → 뒤에서 다룰 ScopedPredictionKey 이슈를 구조적으로 안고 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. 리팩토링 후 구조 — &lt;code&gt;UGA_SkillBase&lt;/code&gt;로 Template Method 통합&lt;/h2&gt;
&lt;p&gt;부모가 흐름 전체를 소유하고, 자식은 &lt;code&gt;OnSkillEffect&lt;/code&gt; 한 곳만 오버라이드하도록 묶었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ActivateAbility
 ├─ LoadActiveSkillSummary      (인벤토리 최종 스탯)
 ├─ CommitAbility
 └─ ExecuteSkillLogic
     ├─ (서버) AbilityTargetDataSetDelegate 바인딩 + CallReplicatedTargetDataDelegatesIfSet
     └─ StartMontageAndTasks
         ├─ WaitGameplayEvent(SkillEventTag)  ──▶ OnFireEventReceived
         │    └─ (로컬) FScopedPredictionWindow + 마우스 좌표 → ServerSetReplicatedTargetData
         │                                     → 클라는 즉시 OnSkillEffect(Target, Aim)
         └─ PlayMontageAndWait  ──▶ OnMontageFinished ──▶ EndAbility&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;자식 GA가 단순해진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GA_Projectile_after&lt;/code&gt;: 지팡이 소켓 기반 스폰 + 서버 권위 &lt;code&gt;SpawnActor&lt;/code&gt; 수준.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GA_SkillField_after&lt;/code&gt;: 5줄 수준. 장판 액터 스폰 후 &lt;code&gt;SMASkillField::InitField&lt;/code&gt;에 모든 이펙트 위임.&lt;/li&gt;
&lt;li&gt;마우스 좌표 획득은 &lt;code&gt;TryGetMouseGroundLocation&lt;/code&gt; 하나로 통일.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;설계 포인트&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AttackMontage&lt;/code&gt;가 없으면 &lt;code&gt;OnFireEventReceived&lt;/code&gt;를 즉시 부르고 &lt;code&gt;EndAbility&lt;/code&gt; → 애니메이션 없는 스킬과 호환.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OnFireEventReceived&lt;/code&gt;는 &lt;strong&gt;로컬 경로에서만&lt;/strong&gt; ServerRPC를 날림 → 자식은 서버/클라 분기를 신경 쓰지 않음.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OnTargetDataReadyCallBack&lt;/code&gt;에서 &lt;strong&gt;&lt;code&gt;ConsumeClientReplicatedTargetData&lt;/code&gt;&lt;/strong&gt; 호출 — 이전 구조에서 빠져 있던 ASC 타겟데이터 큐 메모리 누수 방어.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결과적으로 &lt;code&gt;GA_SkillField&lt;/code&gt;는 110줄 → 40줄, &lt;code&gt;GA_Projectile&lt;/code&gt;은 50줄 → 몇 줄로 줄었고, 앞으로 추가되는 스킬은 템플릿에 얹기만 하면 된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. GameplayCue가 owner 클라에만 안 보이는 문제&lt;/h2&gt;
&lt;p&gt;리팩토링을 진행하면서 &lt;code&gt;GA_SkillField&lt;/code&gt;에 오래전부터 남아 있던 버그 하나가 드러났다.&lt;/p&gt;
&lt;h3&gt;3-1. 배경지식 — LocalPredicted GA와 ScopedPredictionKey&lt;/h3&gt;
&lt;p&gt;이론 배경은 이전에 정리한 글로 갈음한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://hanong8.tistory.com/93&quot;&gt;GameplayCue 멀티플레이 작동 방식&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;핵심만 다시 짚으면,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ASC Replication Mode 3가지&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Full&lt;/strong&gt;: owner 포함 모든 클라에 복제&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mixed&lt;/strong&gt;: owner 제외, 다른 클라에만 복제 (owner는 예측으로 로컬 재생)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal&lt;/strong&gt;: 특정 속성만 선택 복제&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;LocalPredicted GA 내부의 예측키 스코프&lt;/strong&gt;&lt;br&gt;GA의 &lt;code&gt;NetExecutionPolicy&lt;/code&gt;가 &lt;code&gt;LocalPredicted&lt;/code&gt;이면, 서버가 그 GA를 실행하는 동안 &lt;code&gt;UAbilitySystemComponent::ScopedPredictionKey&lt;/code&gt;에 &lt;strong&gt;클라이언트가 생성한 activation prediction key&lt;/strong&gt;가 세팅된다. 이 스코프 안에서 &lt;code&gt;AddGameplayCue&lt;/code&gt;를 호출하면 FastArray 엔트리에 그 key가 붙어 직렬화되고, 복제 로직은 &amp;quot;owner는 이 key로 이미 로컬 예측을 했을 것&amp;quot;으로 판단해 &lt;strong&gt;owner에게는 전송을 스킵&lt;/strong&gt;한다. 다른 클라에게는 정상 전송된다.&lt;/p&gt;
&lt;p&gt;즉, &lt;code&gt;ASC Replication Mode = Full&lt;/code&gt;이라도 &lt;strong&gt;예측키 기반 owner-skip은 별개 경로로 작동&lt;/strong&gt;한다. 이 점이 이번 문제의 뿌리다.&lt;/p&gt;
&lt;h3&gt;3-2. 증상&lt;/h3&gt;
&lt;p&gt;SagoMagic의 &lt;code&gt;ASMPlayerState&lt;/code&gt;는 ASC Replication Mode를 &lt;code&gt;Full&lt;/code&gt;로 설정해 owner 포함 모든 클라에 Cue가 복제되도록 해뒀다. 그런데 장판 스킬만 이상했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;리모트 클라에서는 장판 비주얼이 &lt;strong&gt;항상&lt;/strong&gt; 정상 표시&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;owner(시전자 본인)&lt;/strong&gt; 클라에서만 &lt;strong&gt;간헐적&lt;/strong&gt;으로 비주얼 누락&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;owner 쪽 누락이 의심되어 클라 측 예측 분기(&lt;code&gt;if (!HasAuthority()) ASC-&amp;gt;AddGameplayCue(...)&lt;/code&gt;)를 제거했더니 오히려 &lt;strong&gt;owner 클라가 항상 안 보이게&lt;/strong&gt; 됐다. 다른 클라는 여전히 정상. 이 시점에서 &amp;quot;Full 모드인데 owner만 skip되는&amp;quot; 전형적인 예측키 skip 패턴이라는 확신이 생겼다.&lt;/p&gt;
&lt;h3&gt;3-3. 원인 확정&lt;/h3&gt;
&lt;p&gt;리팩토링 전 코드를 다시 보면 서버 경로에서 직접 &lt;code&gt;AddGameplayCue&lt;/code&gt;를 호출하고 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// GA_SkillField_before.cpp — 서버 경로
UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo();
...
FGameplayCueParameters CueParams;
CueParams.Location = SpawnLocation;
CueParams.RawMagnitude = FieldDuration;
SourceASC-&amp;gt;AddGameplayCue(SMSkillTag::GameplayCue_Skill_SpawnField_Tick, CueParams);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;호출 시점은 &lt;code&gt;UGA_SkillBase::ActivateAbility&lt;/code&gt; → &lt;code&gt;OnSkillEffect&lt;/code&gt; 체인 안. GA가 LocalPredicted라 서버에서 이 콜스택이 실행되는 동안 ASC-&amp;gt;ScopedPredictionKey에는 클라 activation key가 꽂혀 있다. 결과적으로 Cue FastArray 엔트리가 클라 key로 태깅되고, owner에게는 복제가 스킵된다. 실제 owner 클라는 아무것도 예측하지 않았지만 FastArray는 알 방법이 없다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;Full 모드라고 방심하면 안 된다. &lt;code&gt;LocalPredicted&lt;/code&gt; GA 안쪽에서 서버 권위로 &lt;code&gt;AddGameplayCue&lt;/code&gt;를 호출하면 owner-skip이 일어난다.&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;3-4. 해결 후보 2가지&lt;/h3&gt;
&lt;h4&gt;A. 예측키 일시 초기화 (이전 블로그 글에서 소개한 방식)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;FPredictionKey Saved = OwnerASC-&amp;gt;ScopedPredictionKey;
OwnerASC-&amp;gt;ScopedPredictionKey = FPredictionKey();

FGameplayCueParameters CueParams;
CueParams.Location = SpawnLocation;
CueParams.RawMagnitude = FieldDuration;
OwnerASC-&amp;gt;AddGameplayCue(Tag, CueParams);

OwnerASC-&amp;gt;ScopedPredictionKey = Saved;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;호출 직전에 예측키를 빈 key로 교체해 FastArray 엔트리를 &amp;quot;서버 권위 항목&amp;quot;으로 만들고, 호출 후 원래 키로 되돌린다. 기존 코드를 최소한으로 건드린다는 장점이 있다.&lt;/p&gt;
&lt;h4&gt;B. Cue 호출을 GA 바깥으로 이동 (최종 채택)&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;SMASkillField::InitField&lt;/code&gt;는 서버에서 &lt;code&gt;World-&amp;gt;SpawnActor&lt;/code&gt; 이후 호출되는 &lt;strong&gt;일반 액터 메서드&lt;/strong&gt;라 GA 예측키 스코프 바깥이다. 이 지점에서 Cue를 &lt;strong&gt;Duration GE로 래핑&lt;/strong&gt;해 ASC에 붙이면, GE의 정상 복제 경로를 통해 owner를 포함한 모든 클라에 정상 도달한다..&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// SMASkillField::InitField
OwnerASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(InInstigatorActor);
ActiveCueTag = SMSkillTag::GameplayCue_Skill_SpawnField_Tick;

if (HasAuthority() &amp;amp;&amp;amp; IsValid(OwnerASC) &amp;amp;&amp;amp; CueEffectClass)
{
    FGameplayEffectContextHandle Context = OwnerASC-&amp;gt;MakeEffectContext();
    Context.AddOrigin(GetActorLocation());
    Context.AddInstigator(this, this);

    FGameplayEffectSpecHandle Spec = OwnerASC-&amp;gt;MakeOutgoingSpec(CueEffectClass, 1.f, Context);
    if (Spec.IsValid())
    {
        CueEffectHandle = OwnerASC-&amp;gt;ApplyGameplayEffectSpecToSelf(*Spec.Data.Get());
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;제거는 &lt;code&gt;OnDurationExpired&lt;/code&gt;에서 대칭으로 처리:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (IsValid(OwnerASC))
{
    OwnerASC-&amp;gt;RemoveActiveGameplayEffect(CueEffectHandle);
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;3-5. 왜 B를 택했나&lt;/h3&gt;
&lt;p&gt;두 해결책 모두 증상은 없앤다. 그럼에도 B를 고른 이유 세 가지.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;규율 의존성 제거&lt;/strong&gt;&lt;br&gt;A는 &amp;quot;&lt;code&gt;AddGameplayCue&lt;/code&gt;를 LocalPredicted GA 서버 경로에서 호출할 땐 save-clear-restore로 감싼다&amp;quot;는 규칙을 팀 전원이 매번 기억해야 한다. 한 명이라도 잊으면 같은 버그가 다른 GA에서 재발. B는 &lt;strong&gt;호출 지점 자체가 예측키 스코프 밖&lt;/strong&gt;이라 구조적으로 재발이 불가능하다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cue 수명과 액터 수명의 응집&lt;/strong&gt;&lt;br&gt;장판 비주얼은 &lt;code&gt;ASMASkillField&lt;/code&gt; 액터가 살아있는 동안만 보여야 한다. Add도 Remove도 &lt;code&gt;SMASkillField&lt;/code&gt; 안에 두면 &lt;strong&gt;같은 파일, 같은 상태 기계&lt;/strong&gt; 안에서 관리된다. A를 고르면 Add는 GA에, Remove는 액터에 남아 수명 책임이 두 곳으로 쪼개진다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Duration GE 래핑의 부수 효과&lt;/strong&gt;&lt;br&gt;&lt;code&gt;AddGameplayCue&lt;/code&gt; 대신 &lt;code&gt;ApplyGameplayEffectSpecToSelf&lt;/code&gt;로 붙이면 &lt;strong&gt;&lt;code&gt;FActiveGameplayEffectHandle&lt;/code&gt;이라는 취소 가능한 핸들&lt;/strong&gt;을 얻는다. 디스펠/해제/시전자 사망 같은 이벤트에서 &lt;code&gt;RemoveActiveGameplayEffect(Handle)&lt;/code&gt; 한 줄로 거둬들일 수 있어 확장성이 좋다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3-6. 함께 점검한 항목 — GCN 풀링 &lt;code&gt;WhileActive&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;이전 블로그 글에서 짚었듯 &lt;code&gt;AGameplayCueNotify_Actor&lt;/code&gt;는 성능 최적화를 위해 &lt;strong&gt;풀링&lt;/strong&gt;된다. 같은 Cue가 두 번째로 활성화될 때 엔진은 &lt;code&gt;OnActive&lt;/code&gt;가 아닌 &lt;code&gt;WhileActive&lt;/code&gt;를 호출하므로, 이걸 오버라이드하지 않으면 &lt;strong&gt;두 번째 시전부터 이펙트가 아예 보이지 않는&lt;/strong&gt; 별도의 함정이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool WhileActive_Implementation(AActor* MyTarget,
    const FGameplayCueParameters&amp;amp; Parameters)
{
    return OnActive_Implementation(MyTarget, Parameters);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이번 수정 과정에서 &lt;code&gt;GameplayCue_Skill_SpawnField_Tick&lt;/code&gt; 대응 GCN이 이 패턴을 지키고 있는지 같이 확인했다. 예측키 이슈와 풀링 이슈는 증상이 비슷해서(&amp;quot;두 번째부터 안 보인다&amp;quot; / &amp;quot;owner만 안 보인다&amp;quot;) 서로 가려버리기 쉽다. &lt;strong&gt;동시에 점검&lt;/strong&gt;하는 편이 안전하다.&lt;/p&gt;
&lt;h3&gt;3-7. 체크리스트 (회귀 방지)&lt;/h3&gt;
&lt;p&gt;비슷한 증상을 만났을 때는 다음 순서로.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ASC Replication Mode가 &lt;code&gt;Full&lt;/code&gt;인가&lt;/strong&gt; — &lt;code&gt;Mixed&lt;/code&gt;면 owner-skip은 설계상 정상 동작. &lt;code&gt;Full&lt;/code&gt;로 바꿨는데도 owner만 안 보이면 아래 2~4.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GA &lt;code&gt;NetExecutionPolicy&lt;/code&gt;가 &lt;code&gt;LocalPredicted&lt;/code&gt;인가&lt;/strong&gt; — &lt;code&gt;ServerOnly&lt;/code&gt;/&lt;code&gt;ServerInitiated&lt;/code&gt;면 예측키 스코프 이슈는 발생하지 않음.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AddGameplayCue&lt;/code&gt; 호출 지점이 GA &lt;code&gt;ActivateAbility&lt;/code&gt;/&lt;code&gt;OnSkillEffect&lt;/code&gt; 내부인가&lt;/strong&gt; — 그렇다면 예측키 스코프 안이 거의 확실.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;호출을 스폰된 액터/컴포넌트 메서드로 옮길 수 있는가&lt;/strong&gt; — 가능하면 B, 구조 변경이 부담되면 A(예측키 save/clear/restore), 최후에 NetMulticast RPC + 코스메틱 우회.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;동일 Cue를 두 번 시전했을 때도 보이는가&lt;/strong&gt; — 안 보이면 GCN 풀링 &lt;code&gt;WhileActive&lt;/code&gt; 미구현 의심.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;4. 회고&lt;/h2&gt;
&lt;p&gt;프로토타입 단계에선 &amp;quot;Mixed가 기본인데 Full로만 바꾸면 owner 복제 이슈는 끝난다&amp;quot;고 막연히 생각했다. 실제로는 &lt;code&gt;ScopedPredictionKey&lt;/code&gt;라는 별개의 경로가 owner-skip을 일으킬 수 있고, &lt;code&gt;LocalPredicted&lt;/code&gt; 정책을 쓰는 한 모든 GA가 잠재적으로 이 함정을 갖는다. 해결 자체는 간단하지만 &lt;strong&gt;원인을 한 번 정확히 짚어두지 않으면 다른 스킬에서 같은 모양으로 재발&lt;/strong&gt;하는 부류의 버그였다.&lt;/p&gt;
&lt;p&gt;그리고 이 이슈를 찾아낸 계기는 결국 &amp;quot;애니메이션을 추가하려고 열어본 코드가 너무 중복이 많아서&amp;quot; 리팩토링을 시작한 것이었다. 보일러플레이트를 걷어내는 과정은 단순한 코드 정리가 아니라, &lt;strong&gt;숨어 있던 네트워크 버그가 한 곳에 모여 드러나는 기회&lt;/strong&gt;이기도 했다. 앞으로 추가될 스킬들은 통합된 템플릿 위에 얹기만 하면 되고, CCue 호출 규칙도 &amp;quot;액터 메서드에서 GE로 래핑&amp;quot; 한 가지로 정리됐다.&lt;/p&gt;</description>
      <category>프로그래밍/Unreal Engine 5</category>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/95</guid>
      <comments>https://hanong8.tistory.com/95#entry95comment</comments>
      <pubDate>Tue, 21 Apr 2026 20:45:35 +0900</pubDate>
    </item>
    <item>
      <title>[Unreal Engine5] GAS의 GameplayEffectContextHandle과 GameplayEffectSpecHandle</title>
      <link>https://hanong8.tistory.com/94</link>
      <description>&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;계기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SMASkillProjectile에서 투사체가 적에게 맞았을 때 데미지를 적용하는 코드를 작성하다가 두 핸들의 역할이 헷갈렸다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;FGameplayEffectContextHandle ContextHandle = SourceASC-&amp;gt;MakeEffectContext();
ContextHandle.AddInstigator(Avatar, Controller);

FGameplayEffectSpecHandle SpecHandle = SourceASC-&amp;gt;MakeOutgoingSpec(DamageEffectClass, 1.f, ContextHandle);
SpecHandle.Data-&amp;gt;SetSetByCallerMagnitude(SMSkillTag::Data_Damage_Amount, -BaseDamage);

TargetASC-&amp;gt;ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 두 개를 따로 만들어야 하는지, 그냥 하나로 합칠 수 없는지 의문이 생겼다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GameplayEffectContextHandle &amp;mdash; 출처 증명서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;누가, 어떤 상황에서 이 GE를 발동했는가&quot;&lt;/b&gt; 에 대한 메타데이터를 담는 구조체다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FGameplayEffectContextHandle Context = ASC-&amp;gt;MakeEffectContext();
Context.AddInstigator(Avatar, Controller); // 시전자 + 컨트롤러
Context.AddOrigin(GetActorLocation());     // 발동 위치
Context.AddHitResult(HitResult);          // 피격 정보
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GE가 적용될 때 &lt;b&gt;부가 정보를 함께 전달하는 봉투&lt;/b&gt; 역할을 한다. 이 정보는 이후 아래와 같은 곳에서 꺼내 쓴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데미지 계산 시 Instigator 귀속&lt;/li&gt;
&lt;li&gt;GameplayCue의 피격 위치/방향&lt;/li&gt;
&lt;li&gt;어트리뷰트 변경 이벤트에서 누가 때렸는지 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GameplayEffectSpecHandle &amp;mdash; 실행 계획서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;어떤 GE를, 어떤 수치로 적용할 것인가&quot;&lt;/b&gt; 에 대한 실행 설계도다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;FGameplayEffectSpecHandle Spec = ASC-&amp;gt;MakeOutgoingSpec(DamageEffectClass, Level, Context);
Spec.Data-&amp;gt;SetSetByCallerMagnitude(Tag, -100.f); // 수치 런타임 주입
Spec.Data-&amp;gt;DynamicGrantedTags.AddTag(CooldownTag); // 태그 런타임 추가
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ContextHandle을 &lt;b&gt;내부에 포함&lt;/b&gt;하면서, 실제로 적용될 GE의 클래스, 레벨, 수치, 태그 등 모든 정보를 담고 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;둘의 관계&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;ContextHandle          &amp;rarr;    SpecHandle              &amp;rarr;    ApplyGameplayEffectSpecToSelf
(누가 / 어디서 / 어떻게)      (무엇을 / 얼마나 / 어떤 태그)      (실제 적용)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpecHandle이 ContextHandle을 포함하는 구조다. ContextHandle 없이 Spec을 만드는 것도 가능하지만, 그 경우 GE는 적용되어도 시전자 정보가 없어서 Instigator가 None으로 남는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ContextHandle SpecHandle&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;출처 증명서&lt;/td&gt;
&lt;td&gt;실행 계획서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;담는 것&lt;/td&gt;
&lt;td&gt;시전자, 위치, 피격 정보&lt;/td&gt;
&lt;td&gt;GE 클래스, 수치, 태그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;포함 관계&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;ContextHandle을 내부에 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;없으면&lt;/td&gt;
&lt;td&gt;Instigator 추적 불가&lt;/td&gt;
&lt;td&gt;GE 적용 자체가 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ContextHandle은 &quot;누가 쐈는지&quot;, SpecHandle은 &quot;무엇을 얼마나 쐈는지&quot;다.&lt;br /&gt;출처 없이 계획만 있으면 GE는 적용되지만, 누가 쐈는지 게임이 모른다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>프로그래밍/Unreal Engine 5</category>
      <author>hanong8</author>
      <guid isPermaLink="true">https://hanong8.tistory.com/94</guid>
      <comments>https://hanong8.tistory.com/94#entry94comment</comments>
      <pubDate>Mon, 20 Apr 2026 20:22:31 +0900</pubDate>
    </item>
  </channel>
</rss>