<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>개발자 김선호</title>
        <link>https://velog.io/</link>
        <description>프로젝트 진행 과정을 주로 업로드합니다</description>
        <lastBuildDate>Mon, 03 Aug 2026 07:12:32 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>개발자 김선호</title>
            <url>https://velog.velcdn.com/images/dev_sensational/profile/35263486-8437-4a4e-9457-f039e02b5413/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 개발자 김선호. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_sensational" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[개발 블로그 마이그레이션 안내]]></title>
            <link>https://velog.io/@dev_sensational/%EA%B0%9C%EB%B0%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%95%88%EB%82%B4</link>
            <guid>https://velog.io/@dev_sensational/%EA%B0%9C%EB%B0%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%95%88%EB%82%B4</guid>
            <pubDate>Mon, 03 Aug 2026 07:12:32 GMT</pubDate>
            <description><![CDATA[<p><a href="https://devsensational.github.io/">https://devsensational.github.io/</a></p>
<p>개발 블로그를 github로 이전하게 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] iterator, reverse_iterator의 차이점]]></title>
            <link>https://velog.io/@dev_sensational/C-iterator-reverseiterator%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@dev_sensational/C-iterator-reverseiterator%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Sun, 26 Jul 2026 09:29:10 GMT</pubDate>
            <description><![CDATA[<h3 id="라이선스-표기">라이선스 표기</h3>
<blockquote>
</blockquote>
<p>Copyright (c) Microsoft Corporation.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
모든 코드는 표기된 라이센스를 따르며, 설명을 위해 수정된 사항이 있습니다</p>
<h3 id="원문-출처">원문 출처</h3>
<blockquote>
</blockquote>
<p><a href="https://github.com/microsoft/STL/blob/main/stl/inc/iterator">https://github.com/microsoft/STL/blob/main/stl/inc/iterator</a>
<a href="https://github.com/microsoft/STL/blob/main/stl/inc/xutility">https://github.com/microsoft/STL/blob/main/stl/inc/xutility</a>
<a href="https://learn.microsoft.com/ko-kr/cpp/standard-library/reverse-iterator-class?view=msvc-170">https://learn.microsoft.com/ko-kr/cpp/standard-library/reverse-iterator-class?view=msvc-170</a>
<a href="https://learn.microsoft.com/ko-kr/cpp/standard-library/iterator?view=msvc-170">https://learn.microsoft.com/ko-kr/cpp/standard-library/iterator?view=msvc-170</a>
<a href="https://en.cppreference.com/cpp/iterator/reverse_iterator">https://en.cppreference.com/cpp/iterator/reverse_iterator</a></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/0b2670e3-e473-479f-88a7-d1b4729f633f/image.png" alt=""></p>
<p>STL vector에서 <code>rbegin()</code>으로 리턴받은 iterator를 인자로 사용하여 <code>erase()</code>를 시도했지만 타입이 맞지 않는다는 에러가 발생했습니다. </p>
<p><code>begin()</code>은 <code>vector&lt;T&gt;::iterator</code>을 반환하고 <code>rbegin()</code>은 <code>vector&lt;T&gt;::reverse_iterator</code>를 반환하고 있었습니다. 또한, 연산 방향이나 종료 지점에도 차이가 존재했습니다.</p>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>begin()</strong></th>
<th><strong>rbegin()</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>의미</strong></td>
<td>Begin (시작)</td>
<td><strong>Reverse</strong> Begin (역방향 시작)</td>
</tr>
<tr>
<td><strong>시작 위치</strong></td>
<td>첫 번째 요소 (<code>[0]</code>)</td>
<td>마지막 요소 (<code>[N-1]</code>)</td>
</tr>
<tr>
<td><strong>종료 지점</strong></td>
<td><code>end()</code> (마지막 다음 칸)</td>
<td><code>rend()</code> (첫 번째 이전 칸)</td>
</tr>
<tr>
<td><strong><code>++</code> 연산 시</strong></td>
<td>오른쪽으로 이동</td>
<td>왼쪽으로 이동</td>
</tr>
</tbody></table>
<hr>
<h3 id="그럼-reverse_iterator로-erase를-실행하려면-어떻게-해야할까요">그럼 <code>reverse_iterator</code>로 <code>erase()</code>를 실행하려면 어떻게 해야할까요?</h3>
<p>먼저 <code>reverse_iterator</code>에 대해 알아봤습니다.</p>
<p><a href="https://github.com/microsoft/STL/blob/main/stl/inc/xutility">https://github.com/microsoft/STL/blob/main/stl/inc/xutility</a></p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/d3e212ff-d945-4eb9-9c0f-b4c7fa49201c/image.png" alt=""></p>
<p><code>reverse_iterator</code>는 기본적으로 사용하던 양방향 반복자(Bidirectional Iterator, BidIt)를 기본적으로 가지고 있으며, 이를 랩핑하여 구현하고 있습니다.</p>
<pre><code class="language-cpp">_NODISCARD _CONSTEXPR17 _BidIt base() const noexcept(...) {
    return current; // 내부에 들고 있던 원본 iterator를 그대로 반환
}</code></pre>
<p><code>base()</code>는 이 <code>current(기존 iterator)</code>를 그대로 반환합니다. </p>
<p>그럼 <code>rit.base()</code>를 사용하면 <code>erase()</code>를 정상적으로 호출할 수 있을까요? 호출은 할 수 있지만 의도와는 다르게 작동합니다. <strong><code>*rit</code>은 <code>current</code>에서 왼쪽 주소를 역참조할 뿐, 실제 물리적 주소는 의도와 다르기 때문입니다.</strong> </p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/65d2e9fc-506a-4161-a0b9-5d910072847e/image.png" alt=""></p>
<pre><code class="language-cpp">_NODISCARD _CONSTEXPR17 reference operator*() const noexcept(...) {
    _BidIt _Tmp = current; // 현재 들고 있는 iterator(current)를 복사
    return *--_Tmp;        // 복사본을 1칸 앞으로(--) 이동시킨 후 역참조(*)해서 반환
}</code></pre>
<p><strong>따라서, 역참조한 주소의 데이터를 지우고 싶다면 전후에 값을 왼쪽으로 옮겨줘야 합니다.</strong></p>
<h3 id="소스코드-및-결과">소스코드 및 결과</h3>
<pre><code class="language-cpp">int main() {
    vector&lt;int&gt; v = { 10, 20, 30, 40, 50 };

    // 역방향 순회 중 &#39;30&#39;을 삭제하고 싶은 경우
    for (auto rit = v.rbegin(); rit != v.rend(); ) {
        if (*rit == 30) {
            cout &lt;&lt; &quot;반복자를 이동시키지 않았을 때, 반환되는 역참조 값: &quot; &lt;&lt; *rit.base() &lt;&lt; &#39;\n&#39;;

            // rit.base()는 30이 아닌 &#39;40&#39;의 위치를 가리키고 있음
            // 따라서 next(rit).base()를 전달해야 정확히 &#39;30&#39; 위치의 iterator가 됨
            auto it = std::next(rit).base();

            // erase는 삭제 후 다음 위치의 정방향 iterator를 반환하므로,
            // 이를 다시 reverse_iterator로 감싸서 갱신
            rit = vector&lt;int&gt;::reverse_iterator(v.erase(it));
        }
        else {
            ++rit;
        }
    }

    // 결과 출력: 10 20 40 50
    for (int n : v) cout &lt;&lt; n &lt;&lt; &quot; &quot;;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/25ddd1d1-ef56-4db5-9e74-9662b3eaba2b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[수학/알고리즘] Cramer's Rule, determinant ]]></title>
            <link>https://velog.io/@dev_sensational/%EC%88%98%ED%95%99%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Cramers-Rule-determinant</link>
            <guid>https://velog.io/@dev_sensational/%EC%88%98%ED%95%99%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Cramers-Rule-determinant</guid>
            <pubDate>Thu, 23 Jul 2026 09:29:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>교점에 별 만들기
<a href="https://school.programmers.co.kr/learn/courses/30/lessons/87377">https://school.programmers.co.kr/learn/courses/30/lessons/87377</a></p>
</blockquote>
<p>교점을 찾은 후 출력하는 문제였습니다. 처음에는 가감법을 사용해서 풀었지만, 예외 처리와 계수항을 맞추기 위한 과정들이 너무 복잡했습니다. 또한, 들어오는 값의 범위가 -100k ~ 100k이다보니 <code>int</code>를 가볍게 뛰어넘는 값들이 사용됐습니다. 이는 <code>std::lcm</code>을 사용할 때 문제를 발생시켰습니다.</p>
<p>더 효율적으로 정답을 찾을 수 있는 방법은 <strong>크래머 공식</strong>을 사용하는 것이었습니다.</p>
<hr>
<h2 id="행렬식determinant-det">행렬식(Determinant, $\det$)</h2>
<p>크래머 공식에 대해 이해하려면 먼저 행렬식에 대해 이해해야 합니다. </p>
<blockquote>
</blockquote>
<p><strong>(중요)아래 글은 행렬식에 대한 정확한 정의가 아니며, 제가 행렬식을 이해하기 위한 과정입니다. 틀린점에 대해서 피드백을 주시면 감사합니다.</strong></p>
<p>먼저, 행렬식은 정사각 행렬을 스칼라로 바꾸어 주는 함수입니다. </p>
<p>제가 이해한 바로는 행렬식의 결과 값이 모눈종이(좌표계) 격자를 찌그러트리고 늘렸을 때, 변형된 모눈종이가 원본에 비해 얼만큼 변했는가?에 가까운 것 같습니다. 즉, <strong>행렬이 공간을 얼마나 변형시키는가(길이/면적/부피 및 방향의 변화 비율)를 나타내는 수</strong>입니다.</p>
<p>쉽게 이해하기 위해 직선을 사용했습니다(이미지의 좌표평면은 2차원이지만, 1차원이라고 생각하고 봐주셔야 합니다). 좌측의 공간에 대해 $det=2$을 적용한 이미지를 예시로 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/ce88f744-422e-40ef-8d48-973c32b3a155/image.png" alt=""></p>
<p>이미지 처럼 공간 자체가 확대되었기 때문에, 직선(빨간선)은 실제로 2배 길어진 16의 길이를 갖게 됩니다. (0, 8)까지의 무수한 점의 위치가 (0, 16)까지로 변형되었기 때문입니다. 하지만, 변형된 공간의 관점에서 봤을 때는 여전히 좌표 값은 변하지 않은 상태입니다. 좌표는 <strong>기저벡터를 몇개 사용했는가</strong>를 의미하기 때문입니다.</p>
<p>이때, 주의해야 할 점은 1개의 공간만 가지고는 어떻게 변형되었는지는 정확히 알 수 없는 것에 유의해야 합니다. 또한, n차원의 길이/면적/부피가 몇 배가 되었는지에 대해 알려주는 것이기 때문에 모든 방향에 $det$을 곱하는 것이 아닙니다.</p>
<p>예를 들어서, 2차원 공간의 사각형에 $det=2$라고 해서 두 방향의 기저에 모두 2를 곱하면 사각형의 넓이는 4배가 됩니다. (그래서 위 예시 이미지를 x축으로만 늘린 것입니다)
<img src="https://velog.velcdn.com/images/dev_sensational/post/3036e2d8-6997-4f9b-8d0e-c2d4d4709021/image.png" alt=""></p>
<p>또한, 하나의 원본을 기준으로 다른 공간과 비교했을 때 $det$을 통해 변형된 방향와 크기를 알 수 있습니다.</p>
<h3 id="det의-부호">$det$의 부호</h3>
<p>행렬식에서 $det$의 부호는 아래와 같은 규칙을 가집니다.</p>
<blockquote>
</blockquote>
<ul>
<li>$det &gt; 0$ → 방향 유지</li>
<li>$det &lt; 0$ → 방향 반전</li>
<li>$det = 0$ → 공간이 납작하게 찌그러짐</li>
</ul>
<p>예를 들어서, $det = -2$인 경우에는 면적은 2배인데 방향이 반전된 것을 의미합니다.</p>
<p>중요한 것은 $det = 0$일때 입니다. 0이 되면 공간이 납작하게 찌그러진 것과 같이 되어 좌표축 하나가 의미를 잃게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/4ca59ad0-dd16-4767-b44c-8ad491d6fa8e/image.png" alt=""></p>
<h3 id="행렬식-활용하기">행렬식 활용하기</h3>
<p>위에서 <strong>행렬식은 공간을 얼마나 변형시키는가를 나타내는 수</strong>라고 말씀드렸습니다. 이 개념을 이용하면 평행사변형의 넓이도 아주 쉽게 이해할 수 있습니다. 가로와 세로가 각각 1인 단위 정사각형을 특정 행렬로 변환하면 새로운 평행사변형이 만들어지는데, 이때 변화된 넓이 자체가 바로 행렬식의 값($\det$)이 됩니다. </p>
<p>두 2차원 벡터 $a = (a_1, a_2)$와 $b = (b_1, b_2)$가 이루는 평행사변형의 넓이는 두 벡터의 외적 크기, 혹은 삼각함수를 이용해 계산할 수 있습니다.
<img src="https://velog.velcdn.com/images/dev_sensational/post/2327564c-87e0-4306-b17e-ac026701369b/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>$$\text{넓이} = \vert{}a_1 b_2 - a_2 b_1\vert{}$$</p>
<p>이 식을 $2 \times 2$ 행렬 형태로 쓴 것이 바로 행렬식입니다.</p>
<blockquote>
</blockquote>
<p>$$\det \begin{pmatrix} a_1 &amp; b_1 \ a_2 &amp; b_2 \end{pmatrix} = a_1 b_2 - a_2 b_1$$</p>
<hr>
<p>이제 두 일차 방정식(직선)을 보겠습니다.</p>
<blockquote>
</blockquote>
<p>$$\begin{cases} a_1 x + b_1 y = c_1 \ a_2 x + b_2 y = c_2 \end{cases}$$</p>
<p>이 방정식의 목표는 두 직선이 만나는 교점 $(x, y)$를 찾는 것입니다. 이 방정식을 행렬 형태로 바꾸면 다음과 같습니다.</p>
<blockquote>
</blockquote>
<p>$$\begin{pmatrix} a_1 &amp; b_1 \ a_2 &amp; b_2 \end{pmatrix} \begin{pmatrix} x \ y \end{pmatrix} = \begin{pmatrix} c_1 \ c_2 \end{pmatrix}$$</p>
<p>이것을 벡터의 합 형태로 다시 풀어서 써보면</p>
<blockquote>
</blockquote>
<p>$$x \begin{pmatrix} a_1 \ a_2 \end{pmatrix} + y \begin{pmatrix} b_1 \ b_2 \end{pmatrix} = \begin{pmatrix} c_1 \ c_2 \end{pmatrix}$$</p>
<p>이 식을 말로 풀어보면 다음과 같습니다</p>
<blockquote>
</blockquote>
<p>기반이 되는 두 벡터 $A = \begin{pmatrix} a_1 \ a_2 \end{pmatrix}$와 $B = \begin{pmatrix} b_1 \ b_2 \end{pmatrix}$를 각각 $x$배, $y$배 늘려서 더했더니, 목표 벡터 $C = \begin{pmatrix} c_1 \ c_2 \end{pmatrix}$가 되었다. 이때 배율 $x$와 $y$는 얼마인가?&quot;</p>
<p>즉, $det \ne 0$라는 것은 평행사변형이 생성된다는 것을 의미합니다. 반대로 0이 나온다면 교점이 생성되지 않았음을 의미합니다. 이를 활용해서 제가 예외 처리하기 위해 만들었던 복잡한 코드를 간편하게 해결할 수 있습니다.</p>
<h2 id="크래머-공식을-사용해서-교점-찾기">크래머 공식을 사용해서 교점 찾기</h2>
<p>방금 방법으로 교점 여부를 확인했다면, 크래머 공식을 통해 교점의 정확한 위치를 알아낼 수 있습니다.</p>
<p>다시, 두 개의 1차 함수(직선의 방정식)가 다음과 같이 주어졌다고 가정해 보겠습니다.</p>
<blockquote>
</blockquote>
<p>$$a_1 x + b_1 y = c_1$$
$$a_2 x + b_2 y = c_2$$</p>
<p>이를 행렬의 형태로 변환하면 다음과 같습니다.</p>
<blockquote>
</blockquote>
<p>$$\begin{pmatrix} a_1 &amp; b_1 \ a_2 &amp; b_2 \end{pmatrix} \begin{pmatrix} x \ y \end{pmatrix} = \begin{pmatrix} c_1 \ c_2 \end{pmatrix}$$</p>
<p>여기서 크래머 공식을 사용하면 행렬식만으로 교점 $(x, y)$를 쉽게 구할 수 있습니다.</p>
<ol>
<li><p><strong>기본 행렬식 ($D$):</strong> $x, y$의 계수들로 이루어진 행렬식입니다.</p>
<blockquote>
</blockquote>
<p>$$D = a_1 b_2 - a_2 b_1$$</p>
</li>
<li><p><strong>$x$를 위한 행렬식 ($D_x$):</strong> 기본 행렬의 첫 번째 열(x의 계수)을 결과값 $c_1, c_2$로 바꾼 행렬식입니다.</p>
<blockquote>
</blockquote>
<p>$$D_x = c_1 b_2 - c_2 b_1$$</p>
</li>
</ol>
<ol start="3">
<li><strong>$y$를 위한 행렬식 ($D_y$):</strong> 기본 행렬의 두 번째 열(y의 계수)을 결과값 $c_1, c_2$로 바꾼 행렬식입니다.<blockquote>
</blockquote>
$$D_y = a_1 c_2 - a_2 c_1$$</li>
</ol>
<p>최종적으로 교점의 좌표는 각 변수의 행렬식을 기본 행렬식으로 나눈 값입니다.</p>
<blockquote>
</blockquote>
<p>$$x = \frac{D_x}{D}, \quad y = \frac{D_y}{D}$$</p>
<h2 id="코드">코드</h2>
<pre><code class="language-cpp">bool inter(int idx1, int idx2, const vector&lt;vector&lt;int&gt;&gt;&amp; line, arg&amp; args) {
    // 오버플로우 방지를 위해 long long 변환
    long long A = line[idx1][0], B = line[idx1][1], E = line[idx1][2];
    long long C = line[idx2][0], D = line[idx2][1], F = line[idx2][2];

    // 1. 분모(det) 계산
    long long det = A * D - B * C;

    // 평행하거나 일치하면 교점 없음
    if (det == 0) return false;

    // 2. 분자 계산
    long long num_x = B * F - E * D;
    long long num_y = E * C - A * F;

    // 3. 정수로 딱 나누어떨어지는지 확인
    if (num_x % det != 0 || num_y % det != 0) return false;

    // 4. 정수 교점 저장
    args.x = num_x / det;
    args.y = num_y / det;

    return true;
}
</code></pre>
<p>기존처럼 LCM을 구해서 계수를 맞추거나 $x, y$ 계수가 $0$일 때 분기 처리를 수십 줄 작성하는 대신, 이 공식 하나로 수십 줄의 코드를 몇 줄로 축소하고 안전하게 처리할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++/CS] 해시 충돌이 발생하는 key 값이 자신의 value를 찾아가는 방법 (unordered_map) + 체이닝의 캐시 효율]]></title>
            <link>https://velog.io/@dev_sensational/CCS-%ED%95%B4%EC%8B%9C-%EC%B6%A9%EB%8F%8C%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-key-%EA%B0%92%EC%9D%B4-%EC%9E%90%EC%8B%A0%EC%9D%98-value%EB%A5%BC-%EC%B0%BE%EC%95%84%EA%B0%80%EB%8A%94-%EB%B0%A9%EB%B2%95-unorderedmap-%EC%B2%B4%EC%9D%B4%EB%8B%9D%EC%9D%98-%EC%BA%90%EC%8B%9C-%ED%9A%A8%EC%9C%A8</link>
            <guid>https://velog.io/@dev_sensational/CCS-%ED%95%B4%EC%8B%9C-%EC%B6%A9%EB%8F%8C%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-key-%EA%B0%92%EC%9D%B4-%EC%9E%90%EC%8B%A0%EC%9D%98-value%EB%A5%BC-%EC%B0%BE%EC%95%84%EA%B0%80%EB%8A%94-%EB%B0%A9%EB%B2%95-unorderedmap-%EC%B2%B4%EC%9D%B4%EB%8B%9D%EC%9D%98-%EC%BA%90%EC%8B%9C-%ED%9A%A8%EC%9C%A8</guid>
            <pubDate>Wed, 22 Jul 2026 11:10:05 GMT</pubDate>
            <description><![CDATA[<h3 id="라이선스-표기">라이선스 표기</h3>
<blockquote>
</blockquote>
<p>Copyright (c) Microsoft Corporation.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
모든 코드는 표기된 라이센스를 따르며, 설명을 위해 수정된 사항이 있습니다</p>
<hr>
<blockquote>
<p>해시 충돌에 대한 글:
<a href="https://en.wikipedia.org/wiki/Hash_table">https://en.wikipedia.org/wiki/Hash_table</a>
<a href="https://preamtree.tistory.com/20">https://preamtree.tistory.com/20</a></p>
</blockquote>
<p>해시 충돌이 발생하면 개방 주소법이나 체이닝을 통해 해결한다는 것은 익히 들어 알고 있을 것입니다. 그런데 문득 이런 생각이 들었습니다. 충돌이 발생한다는 것은 해시 함수가 return하는 해시 값이 동일하다는 것인데, 어떻게 프로그램이 원하는 값을 정확히 찾아올 수 있냐는 것입니다.</p>
<h1 id="해시-값만-사용하는-것이-아님">해시 값만 사용하는 것이 아님</h1>
<p>해시 테이블은 해시 값 하나만 믿고 데이터를 찾지 않습니다. 충돌이 발생하면 ① 충돌을 해결하는 규칙(개방 주소법 또는 체이닝)에 따라 보관해 두고, 나중에 값을 찾을 때는 <strong>② 주소를 따라간 뒤 &quot;진짜 내가 찾던 키(Key)가 맞는지&quot; 실제 키 값을 직접 비교</strong>하는 과정을 거칩니다.</p>
<p>구체적으로 두 방식이 충돌을 해결하고 원하는 값을 찾아가는 과정은 다음과 같습니다.</p>
<h2 id="1-개방-주소법-open-addressing">1. 개방 주소법 (Open Addressing)</h2>
<p>개방 주소법에서는 해시 테이블의 각 칸에 <strong>데이터(Key, Value)가 직접</strong> 들어갑니다.</p>
<h3 id="동작-방식">동작 방식</h3>
<p>예를 들어 <code>Key A</code>와 <code>Key B</code>가 모두 해시 값 <code>3</code>을 출력했다고 가정해 보겠습니다.</p>
<ul>
<li><strong>저장할 때:</strong></li>
</ul>
<ol>
<li><code>Key A</code>가 먼저 들어와 index <code>3</code>에 저장됩니다.</li>
<li><code>Key B</code>가 들어왔는데 index <code>3</code>이 차 있습니다.</li>
<li>약속된 규칙(예: 다음 칸으로 이동)에 따라 빈 칸인 index <code>4</code>에 <code>Key B</code>를 저장합니다.</li>
</ol>
<ul>
<li><strong>찾아갈 때 (<code>Key B</code>를 검색하는 과정):</strong></li>
</ul>
<ol>
<li><code>Key B</code>를 해시 함수에 넣어 index <code>3</code>을 얻습니다.</li>
<li>index <code>3</code>으로 가보니 <code>Key A</code>가 들어있습니다. <strong>&quot;해시 값은 맞지만, 진짜 키(<code>Key B</code>)가 아니네?&quot;</strong> 하고 확인합니다.</li>
<li>저장할 때 썼던 규칙 그대로 다음 칸(index 4)으로 이동합니다.</li>
<li>index <code>4</code>에서 <code>Key B</code>를 발견하고, 키가 일치하므로 해당 값을 반환합니다.</li>
</ol>
<hr>
<h2 id="2-체이닝-chaining">2. 체이닝 (Chaining)</h2>
<p>체이닝 방식에서 해시 테이블의 각 칸은 데이터 자체가 아니라, <strong>연결 리스트의 첫 번째 노드를 가리키는 포인터</strong>를 가집니다.</p>
<h3 id="동작-방식-1">동작 방식</h3>
<p>마찬가지로 <code>Key A</code>와 <code>Key B</code>의 해시 값이 둘 다 <code>3</code>인 경우입니다.</p>
<ul>
<li><strong>저장할 때:</strong></li>
</ul>
<ol>
<li><code>Key A</code>가 들어와 index <code>3</code>번 방의 리스트에 첫 번째 노드로 들어갑니다. (<code>[Key A]</code>연결)</li>
<li><code>Key B</code>가 들어오면 index <code>3</code>번 방의 리스트 끝(또는 앞)에 추가로 연결합니다. (<code>[Key A] -&gt; [Key B]</code> 연결)</li>
</ol>
<ul>
<li><strong>찾아갈 때 (<code>Key B</code>를 검색하는 과정):</strong></li>
</ul>
<ol>
<li><code>Key B</code>를 해시 함수에 넣어 index <code>3</code>을 얻습니다.</li>
<li>index <code>3</code>번 방으로 찾아가 연결된 리스트를 첫 번째 노드부터 순회합니다.</li>
<li>첫 노드 <code>Key A</code> 확인 $\rightarrow$ &quot;내가 찾던 키가 아님&quot;</li>
<li>다음 노드 <code>Key B</code> 확인 $\rightarrow$ <strong>&quot;진짜 키가 맞음!&quot;</strong> $\rightarrow$ 검색 성공</li>
</ol>
<hr>
<p>결국, 해시 충돌이 발생하더라도 원하는 값을 정확히 찾을 수 있는 이유는 <strong>&quot;해시 값(주소)&quot;뿐만 아니라 &quot;원래의 Key 값&quot;도 함께 저장하여 최종 비교하기 때문</strong>입니다. 단순하면서도 당연한 방법이었습니다. 그럼 자주 사용해왔던 <code>unordered_map</code>은 내부적으로 어떻게 동작할까요? 또한, 체이닝을 사용하는지 개방 주소법을 사용하는지도 알아봤습니다.</p>
<h1 id="c-stdunordered_map">C++ std::unordered_map</h1>
<p><code>unordered_map</code>은 체이닝(Chaining) 방식을 사용합니다. 각 버킷이 단방향 연결 리스트(Singly Linked List) 형태의 노드들을 가리키는 구조로 구현되어 있습니다. *<em>여기서 중요한 것은 해시 충돌 여부와 상관없이 데이터가 들어오면 무조건 리스트 노트가 동적 할당되어 생성된다는 것입니다. *</em></p>
<pre><code class="language-cpp">template &lt;class _Keyty, class _Mappedty&gt;
    pair&lt;iterator, bool&gt; _Insert_or_assign(_Keyty&amp;&amp; _Keyval_arg, _Mappedty&amp;&amp; _Mapval) {
        const auto&amp; _Keyval   = _Keyval_arg;

        // [해시 값 계산] 
        // 입력받은 키(_Keyval)를 해시 함수에 넣어 해시 숫자(_Hashval)를 얻어냅니다.
        const size_t _Hashval = this-&gt;_Traitsobj(_Keyval);

        // [기존 키 존재 여부 확인 및 리스트 순회]
        // _Find_last 함수를 통해 해당 해시값의 버킷으로 찾아가 연결 리스트를 뒤져서
        // 이미 똑같은 키(_Keyval)가 존재하는지 검사합니다.
        auto _Target          = this-&gt;_Find_last(_Keyval, _Hashval);

        // 만약 이미 일치하는 키가 존재한다면 (해시 충돌이 아니라 아예 동일한 키인 경우)
        if (_Target._Duplicate) {
            // 새 노드를 만들지 않고, 기존 노드의 Value(second) 값만 새로 덮어씌우고 종료합니다.
            _Target._Duplicate-&gt;_Myval.second = _STD forward&lt;_Mappedty&gt;(_Mapval);
            return {this-&gt;_List._Make_iter(_Target._Duplicate), false};
        }

        // 최대 사이즈(load factor 등)를 초과했는지 체크합니다.
        this-&gt;_Check_max_size();

        // [새로운 노드 생성 (해시 충돌 여부와 상관없이 무조건 생성)]
        // invalidates _Keyval:
        // _List_node_emplace_op2라는 객체를 통해 새로운 리스트 &#39;노드(Node)&#39;를 동적 할당하여 생성합니다.
        // 이 노드 안에는 키(_Keyval_arg)와 값(_Mapval)이 담기게 됩니다.
        _List_node_emplace_op2&lt;_Alnode&gt; _Newnode(
            this-&gt;_Getal(), _STD forward&lt;_Keyty&gt;(_Keyval_arg), _STD forward&lt;_Mappedty&gt;(_Mapval));

        // 만약 요소를 추가하기 전에 해시 테이블의 크기(버킷 수)를 늘려야 한다면 재해싱(Rehash)을 수행합니다.
        if (this-&gt;_Check_rehash_required_1()) {
            this-&gt;_Rehash_for_1();
            // 버킷 개수가 바뀌었으므로, 새로 만든 노드가 들어갈 위치를 다시 찾습니다.
            _Target = this-&gt;_Find_last(_Newnode._Ptr-&gt;_Myval.first, _Hashval);
        }

        // [체이닝 (리스트에 노드 연결)]
        // _Insert_new_node_before 함수를 호출하여, 방금 생성한 새 노드(_Newnode._Release())를
        // 해당 해시값(_Hashval) 버킷이 가리키는 연결 리스트의 특정 위치(_Target._Insert_before)에 
        // 체이닝(연결) 한 뒤 iterator를 반환합니다.
        return {this-&gt;_List._Make_iter(
                    this-&gt;_Insert_new_node_before(_Hashval, _Target._Insert_before, _Newnode._Release())),
            true};
    }</code></pre>
<pre><code class="language-cpp">_NODISCARD mapped_type&amp; at(const key_type&amp; _Keyval) {
        // [해시 계산 및 타겟 탐색]
        // _Traitsobj(_Keyval)을 통해 해시값을 먼저 구합니다.
        // 그리고 부모 클래스(_Hash)의 _Find_last 함수에 &#39;찾고자 하는 원본 키(_Keyval)&#39;와 &#39;해시값&#39;을 같이 넘깁니다.
        // _Find_last 내부에서는 해시값으로 해당 버킷을 찾은 뒤, 버킷에 연결된 단방향 연결 리스트를 순회하며
        // 리스트 내 각 노드의 키와 전달한 &#39;_Keyval&#39;이 정확히 일치(key_equal)하는지 하나씩 비교합니다.
        const auto _Target = this-&gt;_Find_last(_Keyval, this-&gt;_Traitsobj(_Keyval));

        // [실제 키 일치 확인]
        // 해시값이 같고, 실제 키 값까지 정확히 일치하는 진짜 노드를 찾았다면 
        // _Target._Duplicate에 그 노드의 포인터가 담겨서 돌아옵니다.
        if (_Target._Duplicate) {
            // 찾은 노드의 실제 Value(_Myval.second)를 반환합니다.
            return _Target._Duplicate-&gt;_Myval.second;
        }

        // [탐색 실패]
        // 해시값이 같은 버킷으로 갔지만, 연결 리스트를 다 뒤져봐도
        // 실제 키 값이 일치하는 노드가 없다면 예외(out_of_range)를 던집니다.
        _Xout_of_range(&quot;invalid unordered_map&lt;K, T&gt; key&quot;);
    }
</code></pre>
<p>체이닝 과정을 확인하기 위해 디버깅 모드와 아래 소스코드를 활용했습니다.</p>
<pre><code class="language-cpp">
#include &lt;iostream&gt;
#include &lt;unordered_map&gt;
#include &lt;string&gt;

// 1. 해시 충돌을 발생시키는 커스텀 해시 함수
struct CollisionHash {
    size_t operator()(const int&amp; key) const {
        return 1; // 어떤 키가 들어와도 무조건 해시값 1을 반환! (같은 방 배정)
    }
};

// 2. 키 비교 과정을 관찰하기 위한 커스텀 동등성 비교 함수
struct TraceEqual {
    bool operator()(const int&amp; lhs, const int&amp; rhs) const {
        // 🔴 [디버깅 정지 지점 1]
        bool result = (lhs == rhs);
        std::cout &lt;&lt; &quot;[Trace] 키 비교 중: 기존 노드 키(&quot; &lt;&lt; lhs &lt;&lt; &quot;) vs 찾는 키(&quot; &lt;&lt; rhs &lt;&lt; &quot;) -&gt; &quot; &lt;&lt; (result ? &quot;일치&quot; : &quot;불일치&quot;) &lt;&lt; &quot;\n&quot;;
        return result;
    }
};

int main() {
    std::unordered_map&lt;int, std::string, CollisionHash, TraceEqual&gt; my_map;

    std::cout &lt;&lt; &quot;--- 데이터 삽입 시작 ---\n&quot;;
    my_map[10] = &quot;Apple&quot;;  // 버킷 1에 삽입 (길이 1)

    // 🔴 [디버깅 정지 지점 2] 삽입 시 순회 확인을 원한다면 여기서 Step Into(F11)를 해보세요
    my_map[20] = &quot;Banana&quot;; // 해시가 1이므로 충돌! 리스트를 뒤지며 10과 20을 비교함
    my_map[30] = &quot;Cherry&quot;; // 해시가 1이므로 충돌! 리스트를 뒤지며 10, 20과 30을 순차적으로 비교함

    std::cout &lt;&lt; &quot;\n--- 데이터 탐색 시작 ---\n&quot;;
    // 🔴 [디버깅 정지 지점 3] 탐색 시 순회 확인을 원한다면 여기서 Step Into(F11)를 해보세요
    std::string value = my_map.at(30);
    std::cout &lt;&lt; &quot;찾은 값: &quot; &lt;&lt; value &lt;&lt; &quot;\n&quot;;

    return 0;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/c11a5698-7cf8-46ad-87d6-dcc0cb2b3ee8/image.png" alt=""></p>
<hr>
<p><code>unordred_map</code>은 체이닝을 사용하기 때문에 많은 데이터가 들어왔을 때, 다른 컨테이너에 비해 성능이 그다지 좋지 않습니다(캐시 효율성 문제). 그런데도 왜 체이닝을 사용했는지 이유가 궁금했습니다. 제 궁금증에 대한 답변은 아래 링크에서 얻을 수 있었습니다.</p>
<blockquote>
</blockquote>
<p><a href="https://www.reddit.com/r/cpp/comments/sprdom/why_does_unordered_map_use_chaining_to_prevent/">https://www.reddit.com/r/cpp/comments/sprdom/why_does_unordered_map_use_chaining_to_prevent/</a></p>
<p>번역하자면 다음과 같습니다.</p>
<blockquote>
</blockquote>
<ol>
<li>빈 자리와 이미 차 있는 자리를 구분할 필요가 있습니다.</li>
<li>해시 테이블을 기본 생성자(default constructor)가 있는 타입으로만 제한하여 모든 배열 요소를 미리 생성해 두거나, 아니면 일부 요소는 객체이고 나머지는 가공되지 않은 순수 메모리(raw memory)로 이루어진 배열을 유지해야 합니다.</li>
<li>개방 주소법(Open addressing)은 충돌 관리를 어렵게 만듭니다. 해시 코드가 이미 차 있는 위치에 매핑되는 요소를 삽입하려는 경우, 다음으로 어디를 시도해야 할지 알려주는 정책이 필요합니다. 이는 이미 해결된 문제이지만, 가장 잘 알려진 솔루션들도 복잡합니다.</li>
<li>충돌 관리는 요소를 삭제(erase)할 수 있을 때 특히 더 복잡해집니다. (이와 관련해서는 크누스(Knuth) 교수의 논의를 참조하세요.) 표준 라이브러리용 컨테이너 클래스는 당연히 삭제를 허용해야 합니다.</li>
<li>개방 주소법을 위한 충돌 관리 방식들은 대체로 최대 N개의 요소를 담을 수 있는 고정 크기 배열을 전제로 하는 경향이 있습니다. 표준 라이브러리용 컨테이너 클래스는 사용 가능한 메모리 한도 내에서 새 요소가 삽입될 때마다 필요한 만큼 크기가 늘어날 수 있어야 합니다.</li>
</ol>
<p>현대에는 <code>Swiss Map</code>같은 방식을 이용해서 캐시 효율 문제를 해결했다고 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] C++ sort와 priority_queue의 결과가 반대로 나오는 이유]]></title>
            <link>https://velog.io/@dev_sensational/C-C-sort%EC%99%80-priorityqueue%EC%9D%98-%EA%B2%B0%EA%B3%BC%EA%B0%80-%EB%B0%98%EB%8C%80%EB%A1%9C-%EB%82%98%EC%98%A4%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@dev_sensational/C-C-sort%EC%99%80-priorityqueue%EC%9D%98-%EA%B2%B0%EA%B3%BC%EA%B0%80-%EB%B0%98%EB%8C%80%EB%A1%9C-%EB%82%98%EC%98%A4%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Fri, 17 Jul 2026 07:23:22 GMT</pubDate>
            <description><![CDATA[<h3 id="원문-출처">원문 출처</h3>
<p><a href="https://en.cppreference.com/w/cpp/container/priority_queue">cppreference.com - priority_queue</a>
<a href="https://en.cppreference.com/w/cpp/algorithm/make_heap">cppreference.com - make_heap</a>
<a href="https://en.cppreference.com/w/cpp/named_req/Compare">cppreference.com - Compare</a></p>
<h3 id="라이선스-표기">라이선스 표기</h3>
<blockquote>
</blockquote>
<p><strong>Copyright (c) Microsoft Corporation.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
모든 코드는 표기된 라이센스를 따르며, 설명을 위해 수정된 사항이 있습니다</strong></p>
<hr>
<p>C++ 코테와 개발을 하다 갑자기 궁금한 점이 생겼습니다. <code>sort</code>와 <code>priority_queue</code>에 동일한 비교 함수(예: <code>greater&lt;int&gt;</code>)를 넘겨주었는데, <strong>출력되는 데이터의 정렬 순서가 정반대</strong>로 나오는 현상입니다.</p>
<blockquote>
</blockquote>
<ul>
<li><code>sort(..., std::greater&lt;int&gt;())</code> $\rightarrow$ <strong>내림차순</strong> (큰 값부터)</li>
<li><code>priority_queue&lt;int, vector&lt;int&gt;, greater&lt;int&gt;&gt;</code> $\rightarrow$ <strong>오름차순</strong> (작은 값부터 차례대로 <code>top()</code>)</li>
</ul>
<p>동일한 비교 함수(예: <code>less&lt;&gt;</code> 또는 <code>a &lt; b</code>)를 전달했을 때 결과가 반대로 나오는 핵심 이유는 두 자료구조가 비교 함수의 결과를 해석하고 활용하는 목적이 정반대이기 때문입니다.</p>
<p><code>sort()</code>는 배열을 선형적으로 정렬하기 위해 비교 함수를 &#39;선행 조건&#39;으로 사용하는 반면, <code>priority_queue</code>는 트리(Heap)의 루트에 최댓값을 올리기 위해 비교 함수를 &#39;우선순위 역전 조건&#39;으로 사용합니다.</p>
<h4 id="1-sort의-선형-정렬과-엄격한-약한-순서strict-weak-ordering">1. <code>sort</code>의 선형 정렬과 엄격한 약한 순서(Strict Weak Ordering)</h4>
<p><code>sort</code>는 컨테이너의 요소들을 선형적으로 정렬합니다. C++의 모든 정렬 알고리즘은 <strong>엄격한 약한 순서(Strict Weak Ordering)</strong> 라는 수학적 규칙을 기반으로 동작합니다.</p>
<blockquote>
<p>Strict Weak Ordering에 대해 요약한 블로그 글
<a href="https://hanarotg.tistory.com/224">https://hanarotg.tistory.com/224</a></p>
</blockquote>
<p><code>sort</code>에 전달된 비교 함수 <code>comp(a, b)</code>가 <code>true</code>를 반환한다는 것은 &quot;정렬된 결과에서 a가 b보다 무조건 앞에 위치해야 한다&quot;는 것을 의미합니다.</p>
<blockquote>
</blockquote>
<ul>
<li><strong><code>less&lt;T&gt;</code> 적용 시:</strong> 내부적으로 <code>a &lt; b</code>를 평가합니다. 이 값이 <code>true</code>라면 더 작은 요소가 앞에 배치되므로 최종적으로 <strong>오름차순</strong> 정렬이 됩니다.</li>
<li><strong><code>greater&lt;T&gt;</code> 적용 시:</strong> 내부적으로 <code>a &gt; b</code>를 평가합니다. 더 큰 요소가 앞에 배치되어야 하므로 최종적으로 <strong>내림차순</strong> 정렬이 됩니다.</li>
</ul>
<p>즉, <code>sort</code>에서 비교 연산자는 <strong>요소들의 최종적인 선형 배치 순서 그 자체</strong>를 결정합니다.</p>
<h4 id="2-priority_queue의-힙heap-구성-원리">2. priority_queue의 힙(Heap) 구성 원리</h4>
<p>반면 <code>priority_queue</code>는 내부적으로 트리 기반의 힙(Heap) 자료구조를 유지하기 위해 비교 연산자를 사용합니다. 컨테이너 어댑터인 <code>priority_queue</code>는 요소 삽입과 삭제 시 내부적으로 <code>&lt;algorithm&gt;</code> 헤더의 <code>push_heap</code>과 <code>pop_heap</code> 알고리즘을 호출합니다.</p>
<p>힙을 구성할 때 C++ 표준 알고리즘은 부모 노드와 자식 노드를 비교합니다. 이때 비교 함수 <code>comp(parent, child)</code>가 <code>true</code>를 반환하면, <strong>부모 노드가 자식 노드보다 우선순위가 낮다고 판단하여 두 노드의 위치를 교환(Swap)</strong> 합니다. 즉, 자식 노드를 부모 위치로 끌어올립니다.</p>
<blockquote>
</blockquote>
<ul>
<li><strong><code>less&lt;T&gt;</code> 적용 시:</strong> <code>parent &lt; child</code>가 <code>true</code>일 때 위치를 바꿉니다. 작은 값이 아래로 내려가고 큰 값이 루트 노드(Top)로 올라가게 됩니다. 그 결과, 가장 큰 값이 최상단에 위치하는 <strong>최대 힙(Max-Heap)</strong> 이 형성됩니다.</li>
<li><strong><code>greater&lt;T&gt;</code> 적용 시:</strong> <code>parent &gt; child</code>가 <code>true</code>일 때 위치를 바꿉니다. 큰 값이 아래로 내려가고 작은 값이 루트 노드로 올라갑니다. 결과적으로 가장 작은 값이 최상단에 위치하는 <strong>최소 힙(Min-Heap)</strong> 이 형성됩니다.</li>
</ul>
<hr>
<h3 id="sort가-힙-정렬을-사용하게-되면-priority_queue와는-어떤-차이가-있을까요">sort()가 힙 정렬을 사용하게 되면 priority_queue와는 어떤 차이가 있을까요?</h3>
<p><code>sort()</code>는 최악의 상황에서도 $O(N \log N)$의 시간 복잡도를 보장하면서, 평균적인 성능을 극대화하기 위해 <strong>하이브리드 정렬 알고리즘</strong>을 사용합니다. 퀵, 힙, 삽입 정렬을 기준에 따라 사용하게 되는데, 퀵 정렬을 사용하다가 재귀 깊이가 임계점을 초과하면 힙 정렬로 즉시 전환하게 됩니다.</p>
<pre><code class="language-cpp">
//sort()
//&lt;algorithm&gt;
template &lt;class _RanIt, class _Pr&gt;
_CONSTEXPR20 void _Sort_unchecked(_RanIt _First, _RanIt _Last, _Iter_diff_t&lt;_RanIt&gt; _Ideal, _Pr _Pred) {
    // order [_First, _Last)
    for (;;) {
        if (_Last - _First &lt;= _ISORT_MAX) { // small
            _STD _Insertion_sort_unchecked(_First, _Last, _Pred);
            return;
        }

        // 💡 [분기점] 재귀 깊이 임계값을 초과했을 때 (힙 정렬로 전환)
        // 피벗이 한쪽으로 계속 치우쳐서 퀵 정렬이 최악의 시간복잡도 O(N^2)로 향하고 있다면,
        // 허용된 분할 횟수(_Ideal)가 0 이하가 됩니다. 
        // 이때 즉시 &#39;최소 힙&#39;을 만들고 뒤집어서 강제로 정렬을 마칩니다.
        if (_Ideal &lt;= 0) { // heap sort if too many divisions
            _STD _Make_heap_unchecked(_First, _Last, _Pred); // 1단계: 힙 생성[cite: 1]
            _STD _Sort_heap_unchecked(_First, _Last, _Pred); // 2단계: 뒤에서부터 재배치[cite: 1]
            return;
        }

        auto _Mid = _STD _Partition_by_median_guess_unchecked(_First, _Last, _Pred);

        _Ideal = (_Ideal &gt;&gt; 1) + (_Ideal &gt;&gt; 2); // allow 1.5 log2(N) divisions


        if (_Mid.first - _First &lt; _Last - _Mid.second) { 
            _STD _Sort_unchecked(_First, _Mid.first, _Ideal, _Pred);
            _First = _Mid.second; // 다음 루프에서는 우측 영역을 대상으로 시작
        } else { 
            _STD _Sort_unchecked(_Mid.second, _Last, _Ideal, _Pred);
            _Last = _Mid.first; // 다음 루프에서는 좌측 영역을 대상으로 시작
        }
    }
}</code></pre>
<p>이때, 힙을 생성하는 과정은 <code>priority_queue</code>와 같은 방법을 사용합니다. (__msvc_heap_algorithm.hpp)</p>
<pre><code class="language-cpp">//priority_queue
//&lt;queue&gt;
void _Make_heap() {
    _STD make_heap(c.begin(), c.end(), _STD _Pass_fn(comp));
}

// __msvc_heap_algorithms.hpp
_EXPORT_STD template &lt;class _RanIt, class _Pr&gt;
_CONSTEXPR20 void make_heap(_RanIt _First, _RanIt _Last, _Pr _Pred) { // make [_First, _Last) into a heap
    _STD _Adl_verify_range(_First, _Last);

    // ★ 같은 함수를 사용하는 지점
    _STD _Make_heap_unchecked(_STD _Get_unwrapped(_First), _STD _Get_unwrapped(_Last), _STD _Pass_fn(_Pred));
}

//sort()
//&lt;algorithm&gt;
_CONSTEXPR20 void _Sort_unchecked(_RanIt _First, _RanIt _Last, _Iter_diff_t&lt;_RanIt&gt; _Ideal, _Pr _Pred) {
(...)
if (_Ideal &lt;= 0) { // heap sort if too many divisions
            // ★ 같은 함수를 사용하는 지점
            _STD _Make_heap_unchecked(_First, _Last, _Pred); // 1단계: 힙 생성[cite: 1]
            _STD _Sort_heap_unchecked(_First, _Last, _Pred); // 2단계: 뒤에서부터 재배치[cite: 1]
            return;
        }
(...)
}</code></pre>
<p>순서가 뒤집히는 분기점은 <code>_Sort_heap_unchecked</code>였습니다. 정렬이 완료된 후, 힙에 쌓인 데이터를 배열에 재배치할 때 차이가 발생합니다.</p>
<pre><code class="language-cpp">template &lt;class _RanIt, class _Pr&gt;
_CONSTEXPR20 void _Sort_heap_unchecked(_RanIt _First, _RanIt _Last, _Pr _Pred) {
    // 힙 구조가 완성된 상태에서 맨 마지막 원소(_Last - 1)부터 역방향으로 진행합니다.
    for (; 2 &lt;= _Last - _First; --_Last) {

        // 핵심 분기점: 루트 노드(_First)에 있는 최우선순위 값을 
        // 배열의 맨 마지막 칸(_Last - 1)으로 이동시키고 영역을 1칸 줄입니다.
        _STD _Pop_heap_unchecked(_First, _Last, _Pred);
    }
}</code></pre>
<p>이 루프가 작동하는 과정은 다음과 같습니다.</p>
<blockquote>
</blockquote>
<p><strong>첫 번째 루프:</strong> 현재 최소 힙의 루트(0번 칸)에 있는 가장 작은 값을 뽑아내어 배열의 맨 마지막 칸에 채워 넣습니다.
<strong>두 번째 루프:</strong> 남은 원소들로 다시 최소 힙을 구성하면 루트에 두 번째로 작은 값이 올라옵니다. 이 값을 다시 배열의 뒤에서 두 번째 칸에 채워 넣습니다.
<strong>반복 결과:</strong> 가장 작은 값들이 배열의 뒤쪽 공간부터 역순으로 차곡차곡 쌓이게 됩니다.</p>
<p>최종적으로 배열의 앞쪽에는 큰 값들이 남고, 배열의 맨 뒤에 가장 작은 값이 배치되므로 전체 배열을 출력했을 때 내림차순의 형태를 띠게 되는 것입니다.</p>
<p>결국 동일한 기준(e.g. comp 함수로 <code>greater&lt;&gt;</code>를 사용)을 적용하더라도 결과가 반대인 이유는 인출 및 저장 메커니즘의 차이 때문이었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] explicit 키워드에 대한 정리]]></title>
            <link>https://velog.io/@dev_sensational/C-explicit-%ED%82%A4%EC%9B%8C%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@dev_sensational/C-explicit-%ED%82%A4%EC%9B%8C%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 04 Jul 2026 09:52:50 GMT</pubDate>
            <description><![CDATA[<p>원문 출처
<a href="https://quuxplusone.github.io/blog/2023/04/08/most-ctors-should-be-explicit/">https://quuxplusone.github.io/blog/2023/04/08/most-ctors-should-be-explicit/</a></p>
<hr>
<h2 id="1-컴파일러의-과도한-친절-암시적-변환implicit-conversion">1. 컴파일러의 과도한 친절: 암시적 변환(Implicit Conversion)</h2>
<p>C++ 컴파일러는 친절하게 설계된 부분들이 많습니다. 함수나 연산이 특정 타입의 객체를 요구할 때, 코더가 다른 타입을 넘겨주면 컴파일러는 에러를 내뿜기 전에 <strong>어떻게든 객체를 변환해서 코드를 동작시키려고 시도</strong>합니다.</p>
<p>문제는 이 친절함이 개발자의 의도와 다르게 작동할 때 발생합니다.</p>
<p>아래는 게임이나 서비스에서 특정 고유 ID를 가진 엔티티(Entity) 객체를 다룰 때 발생할 수 있는 문제의 예시입니다.</p>
<pre><code class="language-cpp">class Player {
public:
    // 고유 ID를 받아서 플레이어 객체를 생성 (데이터베이스에서 불러온다고 가정)
    Player(int id) {
        cout &lt;&lt; id &lt;&lt; &quot;번 유령 플레이어 임시 생성됨!\n&quot;;
    }
};

// 특정 플레이어에게 데미지를 입히는 함수
void attack(Player target) {
    cout &lt;&lt; &quot;타겟을 공격했습니다!\n&quot;;
}

int main() {
    int damageAmount = 50;

    // 개발자가 실수로 타겟 객체가 아닌 &#39;데미지 수치&#39;를 함수에 넣어버림
    attack(damageAmount); 
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/5cb169e4-12c1-44ea-9f88-0f0858be913d/image.png" alt=""></p>
<p>상식적으로 attack() 함수는 Player 객체를 요구하므로, 숫자 50을 넣으면 컴파일러가 타입이 맞지 않는다며 에러를 뱉어야 정상일 것입니다.</p>
<p>하지만 explicit이 없으면 컴파일러는 스스로 이렇게 생각합니다.
&quot;어? Player를 달라고 했는데 숫자 50을 줬네? 잠깐, Player 클래스에 숫자를 받아서 객체를 만드는 생성자가 있잖아? 내가 알아서 Player(50)을 만들어서 넘겨줄게!&quot;</p>
<p>결과적으로 에러는 나지 않고, 50번 아이디를 가진 유령 플레이어가 허공에 생성된 뒤 공격을 받게 됩니다. <strong>의도와 맞지 않는 명령이 실행되면서, 나중에 원인을 찾기 위해 밤을 새우게 만드는 최악의 논리 버그가 됩니다.</strong></p>
<p>프로그램이 죽지는 않지만 이상하게 동작하는, 가장 디버깅하기 힘든 부류의 버그가 탄생하는 순간입니다.</p>
<p>만약, 생성자에 explicit Player(int id)를 붙였다면 애초에 attack(damageAmount)에서 컴파일 에러를 내주었을 것입니다.</p>
<h2 id="2-explicit-키워드의-마법">2. explicit 키워드의 마법</h2>
<p>생성자 앞에 <code>explicit</code>을 붙이면 컴파일러의 이런 자동 형변환을 금지할 수 있습니다. <strong>&quot;내가 명시적으로 객체를 생성한다고 적지 않는 한, 네 맘대로 변환하지 마!&quot;</strong>라고 컴파일러에게 선언하는 것입니다.</p>
<pre><code class="language-cpp">class MyString {
public:
    explicit MyString(int size) { /* ... */ }
    // ...
};

printString(10); // ❌ 컴파일 에러 발생! (Cannot convert &#39;int&#39; to &#39;MyString&#39;)

printString(MyString(10)); // ✅ 의도를 명확하게 밝혀야 통과됨
</code></pre>
<p>런타임에 발생할 논리적 버그를 <strong>컴파일 타임 에러</strong>로 끌어올려 즉각적으로 수정할 수 있게 해줍니다.</p>
<h2 id="3-실제-상황에서-발생할-수-있는-치명적인-문제-예시">3. 실제 상황에서 발생할 수 있는 치명적인 문제 예시</h2>
<p><code>explicit</code>이 없을 때 발생한 사례를 찾아보았습니다.</p>
<h3 id="a-예상치-못한-오버로딩-꼬임">A. 예상치 못한 오버로딩 꼬임</h3>
<p><a href="https://www.reddit.com/r/cpp/comments/1hf4z7p/i_am_confused_as_to_when_to_use_explicit_in_a/?tl=ko">https://www.reddit.com/r/cpp/comments/1hf4z7p/i_am_confused_as_to_when_to_use_explicit_in_a/?tl=ko</a></p>
<p>Reddit C++ 커뮤니티의 한 유저는 포인터가 암시적으로 <code>bool</code>로 변환되는 C++의 특성 때문에 버그를 겪은 사례를 공유했습니다.</p>
<p>예시로 어떤 함수 <code>Foo(bool)</code>과 <code>Foo(MyClass)</code>가 있을 때, 포인터를 넘겼더니 의도치 않게 <code>bool</code> 관련 로직이나 엉뚱한 암시적 생성자를 타고 들어가는 식의 오버로딩 꼬임 현상이 발생하였다고 합니다. </p>
<p><code>explicit</code>은 타입 간의 조용한 변환을 막아 오버로딩 모호성을 줄이므로 이런 문제를 예방할 수 있습니다.</p>
<h3 id="b-숨겨진-성능-저하-hidden-performance-costs">B. 숨겨진 성능 저하 (Hidden Performance Costs)</h3>
<p>객체를 생성하는 데 비용이 많이 드는(Heavy) 클래스라면 암시적 변환은 치명적인 성능 저하를 유발합니다.</p>
<pre><code class="language-cpp">void processData(const BigDataContainer&amp; data);

// 만약 BigDataContainer(int id)가 explicit이 아니라면?
processData(1004); // int 하나만 넘겼는데 뒤에서 수십 MB 메모리 할당이 일어날 수 있음
</code></pre>
<p><code>explicit</code>을 사용하면 사용자가 <code>processData(BigDataContainer(1004))</code>처럼 명시적으로 코드를 작성해야 하므로, <strong>무거운 객체가 여기서 생성 된다</strong>라는 것을 인지할 수 있게 됩니다.</p>
<h3 id="c-c11-이후-다중-인자와-중괄호-초기화의-함정">C. C++11 이후: 다중 인자와 중괄호 초기화의 함정</h3>
<p>과거에는 &quot;인자가 1개인 생성자&quot;만 조심하면 됐지만, C++11의 중괄호 초기화(Braced Initialization)가 도입되면서 다중 인자 생성자에서도 암시적 변환이 일어납니다.</p>
<pre><code class="language-cpp">struct Rectangle {
    Rectangle(int width, int height) {}
};
void draw(Rectangle r);

draw({10, 20}); // 암시적 변환 허용. 
</code></pre>
<p>편리해 보이지만, 코드가 복잡해질수록 <code>{10, 20}</code>이 무엇을 의미하는지 문맥상 파악하기 힘들어집니다. 생성자를 <code>explicit Rectangle(int, int)</code>로 만들면 <code>draw(Rectangle{10, 20})</code>으로 작성하도록 강제할 수 있어 코드의 가독성과 안전성이 높아집니다.</p>
<h3 id="d-배열-크기와-데이터-값의-혼동">D. 배열 크기와 데이터 값의 혼동</h3>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class IntArray {
public:
    // 숫자를 하나 받아서 &quot;그 숫자만큼의 크기&quot;를 가진 빈 배열을 만듦
    IntArray(int size) {
        cout &lt;&lt; &quot;크기가 &quot; &lt;&lt; size &lt;&lt; &quot;인 빈 배열 생성\n&quot;;
    }
};

// 학생들의 시험 점수 배열을 받아서 평균을 계산하는 함수
void calculateAverage(const IntArray&amp; scores) {
    // ... 평균 계산 로직 ...
}

int main() {
    int myScore = 95;

    // 개발자의 의도: &quot;내 점수(95) 하나만 일단 넣어서 계산해볼까?&quot;
    calculateAverage(myScore); 
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/3f91d62e-56b2-4eae-b222-5ab12fd0764e/image.png" alt=""></p>
<p>개발자는 95점이라는 <strong>int형 데이터</strong> 하나를 전달하고 싶었습니다. 하지만 컴파일러는 또다시 친절함을 발휘해 IntArray(95)를 호출해 버립니다.</p>
<p>결과적으로 95점이라는 데이터가 전달된 것이 아니라, 크기가 95칸인 텅 빈 0점짜리 배열이 함수로 넘어가게 됩니다. 프로그램은 죽지 않고 평균 0점이라는 엉뚱한 결과를 출력하게 됩니다.</p>
<h2 id="4-explicit-사용의-골든-룰-언제-쓰고-언제-생략할까">4. explicit 사용의 골든 룰 (언제 쓰고, 언제 생략할까?)</h2>
<p>Arthur O&#39;Dwyer는 &quot;몇 가지 예외를 제외한 99%의 생성자에는 explicit을 붙이는 것이 옳다&quot;고 강조합니다. C++의 기본값은 암시적 변환 허용이지만, 이는 과거 C언어와의 호환성 때문이며 현대적인 소프트웨어 공학 관점에서는 모든 생성자에 <code>explicit</code>을 기본으로 다는 것이 좋습니다.</p>
<p>하지만 <strong>반드시 <code>explicit</code>을 생략해야 하는 예외 상황</strong>들도 있습니다.</p>
<ol>
<li><strong>복사 및 이동 생성자</strong>
<code>MyClass(const MyClass&amp;)</code> 같은 복사 생성자를 명시적으로 만들면 <code>MyClass a = b;</code> 와 같은 기본적인 대입조차 막히게 되므로 절대 붙여서는 안 됩니다.</li>
<li><strong><code>std::initializer_list</code>를 받는 생성자</strong>
<code>std::vector&lt;int&gt; v = {1, 2, 3};</code> 처럼 배열 형태로 값을 집어넣어 초기화하는 컨테이너 타입들은 암시적 변환이 그 자체의 목적이므로 생략해야 합니다.</li>
<li><strong>단순 데이터 묶음 (C-Struct 대체품)</strong>
<code>std::pair</code>나 <code>std::tuple</code>처럼 순수하게 데이터를 담는 바구니 역할만 하는 구조체라면 중괄호 <code>{}</code>를 통한 암시적 변환을 허용하는 것이 자연스럽습니다.</li>
<li><strong>본질적으로 같은 데이터를 표현하는 경우</strong>
<code>const char*</code>에서 <code>std::string</code>으로 변환되거나, <code>int</code>에서 직접 만든 <code>BigInt</code> 클래스로 변환되는 것처럼 &quot;의미론적으로 완전히 동일한 개념&quot;일 때는 묵시적 변환이 편리합니다.</li>
</ol>
<hr>
<h3 id="💡-operator-bool에도-explicit을-붙이세요">💡 <code>operator bool</code>에도 explicit을 붙이세요!</h3>
<p>생성자 외에도 <code>explicit</code>이 빛을 발하는 곳이 바로 형변환 연산자입니다. 객체가 유효한지 검사하기 위해 <code>operator bool</code>을 정의하는 경우가 많습니다.</p>
<pre><code class="language-cpp">class SmartPtr {
public:
    explicit operator bool() const { return ptr != nullptr; }
    // ...
};
</code></pre>
<p>여기에 <code>explicit</code>을 붙이면 <code>if (myPtr)</code> 같이 조건문 안에서는 정상적으로 작동(Contextual conversion)하지만, <code>int x = myPtr + 1;</code> 처럼 포인터가 실수로 숫자로 변환되어 연산되는 대참사는 컴파일러가 막아줍니다. 아래는 예시 코드를 실제 IDE에 적용한 것입니다.</p>
<h4 id="explicit-키워드-사용-전">explicit 키워드 사용 전</h4>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/ec1ceb72-9049-4e53-bf04-776860a40349/image.png" alt=""></p>
<h4 id="explicit-키워드-사용-후">explicit 키워드 사용 후</h4>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/7480a37d-fe81-4d32-9cf8-34fecf8c88fd/image.png" alt=""></p>
<hr>
<p>명확한 이유가 없다면 <strong>작성하는 모든 생성자(심지어 파라미터가 없는 기본 생성자 포함)에 일단 <code>explicit</code>을 붙이는 습관</strong>을 들이는 것이 더 안전한 C++ 코드를 작성하는 지름길이라는 것을 깨달았습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] 프로젝트 종료]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A2%85%EB%A3%8C</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A2%85%EB%A3%8C</guid>
            <pubDate>Tue, 20 Jan 2026 05:56:49 GMT</pubDate>
            <description><![CDATA[<p>2025년 10월 28일 부터 시작되어 1월 12일을 끝으로, 2개월 이상 진행한 Project Arc가 종료되었습니다. </p>
<p>완성도와 결과만 놓고 보면 아쉬운 점도 분명 존재하지만, 그럼에도 불구하고 개인적으로는 최선을 다해 임했다고 말할 수 있는 프로젝트였습니다.</p>
<hr>
<h3 id="내가-맡았던-역할과-초기-방향성">내가 맡았던 역할과 초기 방향성</h3>
<p>프로젝트 초반, Project Arc는 <strong>PvPvE 기반의 게임</strong>을 목표로 기획되었고, 저는 다음과 같은 역할을 중심으로 참여할 예정이었습니다.</p>
<ul>
<li>절차적 맵 생성 시스템</li>
<li>반복 플레이를 고려한 시스템 단위 설계</li>
<li>전투 외 콘텐츠 확장을 위한 기반 기능 구현</li>
</ul>
<p>특히 <strong>절차적 맵 생성</strong>은 프로젝트의 핵심 차별점 중 하나로 논의되었고, 저 역시 해당 기능을 중심으로 기술적 기여를 준비하고 있었습니다.</p>
<hr>
<h3 id="기획-변경과-역할-축소">기획 변경과 역할 축소</h3>
<p>그러나 프로젝트 중반, 게임의 방향성이 <strong>PvPvE → 내러티브 중심 PvE</strong>로 크게 변경되면서 상황이 달라졌습니다.</p>
<p>이 변화로 인해:</p>
<ul>
<li>절차적 맵 생성은 더 이상 프로젝트 방향성과 맞지 않게 되었고</li>
<li>시스템 중심의 확장 기능보다는
고정된 레벨과 연출, 스토리 전달이 우선시되었습니다</li>
</ul>
<p>그 결과, 제가 초기에 준비하고 있던 역할과 기여 범위가 축소되어 많은 아쉬움이 남았습니다.</p>
<hr>
<h3 id="구현했지만-사용되지-못한-기능들">구현했지만 사용되지 못한 기능들</h3>
<p>프로젝트 기간의 압박 또한 아쉬움으로 남았습니다.</p>
<ul>
<li>NPC 기반 상호작용 시스템</li>
<li>상점(Shop) 기능</li>
</ul>
<p>위 기능들은 실제로 <strong>구현까지 완료</strong>했지만, 전체 일정과 콘텐츠 우선순위 문제로 인해 최종 빌드에는 포함되지 못했습니다.</p>
<p>기능 자체보다는,</p>
<blockquote>
<p><em>“만들었지만 쓰이지 못했다”</em>
는 점이 개인적으로 가장 아쉬웠던 부분이었습니다.</p>
</blockquote>
<hr>
<h3 id="그럼에도-불구하고-얻은-것들">그럼에도 불구하고 얻은 것들</h3>
<p>다만, 이 프로젝트가 <strong>실패나 손해만 남긴 경험</strong>이었다고 생각하지는 않습니다.</p>
<p>오히려 다음과 같은 점에서 의미 있는 경험이었습니다.</p>
<ul>
<li>한 프로젝트 안에서 <strong>NPC, 상점, 대화, 시스템 구조 설계</strong> 등 다양한 영역을 직접 구현해볼 수 있었던 점</li>
<li>기획 변경이라는 현실적인 상황 속에서 개발자가 어떻게 대응해야 하는지를 체감한 점</li>
<li>“내가 구현한 기능이 왜 필요 없어졌는지”를 감정이 아닌 <strong>기획적 관점</strong>에서 이해하려 노력했던 경험</li>
</ul>
<p>이 경험들은 단기적인 결과보다,
<strong>미래의 나에게 분명히 도움이 될 자산</strong>이라고 생각합니다.</p>
<hr>
<h3 id="향후-개선할-수-있었던-점">향후 개선할 수 있었던 점</h3>
<p>회고를 통해 정리해본, 다음 프로젝트에서 반드시 개선하고 싶은 부분은 다음과 같습니다.</p>
<ol>
<li><strong>기획 변경 리스크를 고려한 역할 설계</strong><ul>
<li>특정 기능 하나에 과도하게 의존하기보다는 변경에 대응 가능한 범용적인 기여 구조를 가져갈 필요성을 느꼈습니다.</li>
</ul>
</li>
</ol>
<ol start="2">
<li><strong>조기 통합 및 우선순위 협의</strong><ul>
<li>“나중에 쓰일 기능”이 아니라 <em>“지금 당장 프로젝트에 들어갈 기능”</em> 위주로
팀과 더 적극적으로 조율했어야 했다고 생각합니다.</li>
</ul>
</li>
</ol>
<ol start="3">
<li><strong>기술 구현의 목적 명확화</strong><ul>
<li>단순히 구현하는 것에서 끝나는 것이 아니라 “이 기능이 프로젝트에 어떤 가치를 주는가”를 더 명확히 설명하고 공유했어야 했습니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="마치며">마치며</h3>
<p>Project Arc는 결과만 놓고 보면 아쉬움이 남는 프로젝트였지만, 과정만큼은 분명히 <strong>성장으로 이어진 시간</strong>이었습니다.</p>
<p>기획은 언제든 바뀔 수 있고, 개발자의 역할 역시 그에 따라 변할 수 있다는 현실 속에서 <strong>유연하게 사고하고, 다음을 준비하는 태도</strong>의 중요성을 배웠습니다.</p>
<p>이 프로젝트에서의 경험과 시행착오는 다음 프로젝트에서 더 나은 선택을 하기 위한 밑거름이 될 것이라 믿습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] Seamless Travel 시 ISM과 Raytracing 충돌 문제 해결]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-Seamless-Travel-%EC%8B%9C-ISM%EA%B3%BC-Raytracing-%EC%B6%A9%EB%8F%8C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-Seamless-Travel-%EC%8B%9C-ISM%EA%B3%BC-Raytracing-%EC%B6%A9%EB%8F%8C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Mon, 12 Jan 2026 07:13:29 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에 레이트레이싱을 적용한 뒤, <strong>심리스 트래블 시 Instanced Static Mesh(ISM) 때문에 발생하던 크래시</strong>를 추적하고 해결한 과정을 정리해 두려고 합니다. 동일한 구조의 네트워크/멀티플레이 프로젝트에서 레이트레이싱을 켜면 비슷한 문제를 겪을 수 있기 때문에, 원인과 대응 패턴을 기억해 두면 좋겠다는 생각이 들었습니다.</p>
<hr>
<h2 id="문제-상황-정리">문제 상황 정리</h2>
<ul>
<li>증상<ul>
<li>레이트레이싱 옵션을 활성화한 상태에서 <strong>심리스 트래블(SeamlessTravel)</strong> 을 수행하면 간헐적으로 게임이 크래시.</li>
<li>에디터 PIE나 레이트레이싱 OFF 환경에서는 잘 동작.</li>
</ul>
</li>
</ul>
<ul>
<li>크래시 로그 핵심 부분 (요약)<ul>
<li>Assertion 실패:<ul>
<li><code>Array index out of bounds: 1 into an array of size 1</code></li>
</ul>
</li>
<li>콜스택 상 중요한 함수:<ul>
<li><code>FInstancedStaticMeshSceneProxy::GetDynamicRayTracingInstances()</code></li>
<li><code>RayTracing::FDynamicRayTracingInstancesContext::GatherDynamicRayTracingInstances_Internal()</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>로그 일부:</li>
</ul>
<pre><code>```text
Assertion failed: (Index &gt;= 0) &amp; (Index &lt; ArrayNum)
[File:...Array.h] [Line: 1067]
Array index out of bounds: 1 into an array of size 1
...
FInstancedStaticMeshSceneProxy::GetDynamicRayTracingInstances()
RayTracing::FDynamicRayTracingInstancesContext::GatherDynamicRayTracingInstances_Internal()
Crash in runnable thread Foreground Worker #1
```</code></pre><ul>
<li>로그에서 볼 수 있는 사실<ul>
<li>크래시가 <strong>레이트레이싱용 인스턴스 수집 과정</strong>(<code>GatherDynamicRayTracingInstances</code>)에서 발생.</li>
<li>특히 <code>FInstancedStaticMeshSceneProxy::GetDynamicRayTracingInstances()</code> 내부에서 ISM 관련 배열 접근 시 <strong>인덱스 범위가 꼬인 상태</strong>.</li>
<li>즉, <strong>InstancedStaticMesh 의 내부 상태(인스턴스 배열)가 심리스 트래블 전후 과정에서 깨졌을 가능성</strong>이 매우 높음.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="원인-분석">원인 분석</h2>
<ul>
<li>전제<ul>
<li>InstancedStaticMeshComponent(ISM)는 <strong>각 월드(서버/클라)</strong> 에 존재.</li>
<li>레이트레이싱은 각 클라이언트의 <strong>로컬 월드</strong> 안에 있는 ISM/StaticMesh 를 기준으로 씬을 빌드.</li>
</ul>
</li>
</ul>
<ul>
<li>추정 원인<ul>
<li>심리스 트래블 시, 이전 월드에서 사용하던 ISM 인스턴스들이 <strong>완전히 정리되지 않은 상태</strong>에서<ul>
<li>월드가 전환되거나, 레이트레이싱 씬이 갱신되면서</li>
<li>내부 <code>PerInstanceRenderData</code> / 인스턴스 배열 크기 정보가 어긋난 상태로 참조되는 상황.</li>
</ul>
</li>
<li>그 결과, <code>FInstancedStaticMeshSceneProxy::GetDynamicRayTracingInstances()</code> 가<ul>
<li><code>ArrayNum == 1</code> 인 배열을 <code>Index == 1</code>로 접근하는 식의 <strong>out-of-bounds 접근</strong>을 시도하다가 Assert 에 걸림.</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>왜 심리스 트래블에서만 티가 났는지<ul>
<li>일반적인 레벨 로드(OpenLevel)에서는 월드와 렌더링 관련 리소스가 비교적 깨끗하게 재생성/파괴됨.</li>
<li>심리스 트래블은 <strong>플레이어 관련 객체(Controller, PlayerState 등)를 유지한 채 월드만 교체</strong>하기 때문에<ul>
<li>일부 렌더링/ISM 관련 리소스가 <strong>월드 전환 타이밍에 완전히 정리되지 않고 남는</strong> 경우가 발생할 수 있음.</li>
</ul>
</li>
<li>특히 레이트레이싱은 별도의 동적 인스턴스 수집/관리 경로를 타기 때문에, 이런 내부 상태 꼬임이 Assert 로 바로 드러남.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="해결-전략">해결 전략</h2>
<ul>
<li>목표<ul>
<li><strong>심리스 트래블 직전/직후에 각 월드의 ISM 상태를 확실하게 정리</strong>해서,</li>
<li>레이트레이싱용 인스턴스 수집 시 깨진 데이터를 참조하지 않도록 만들기.</li>
</ul>
</li>
</ul>
<ul>
<li>고려한 방향들<ol>
<li>ISM 을 소유한 액터의 <code>EndPlay</code> / <code>BeginDestroy</code> 에서 개별적으로 정리<ul>
<li>가장 이상적인 구조지만, 현재 프로젝트 규모 상 모든 ISM 사용처를 일일이 추적하기에는 비용이 큼.</li>
</ul>
</li>
<li>월드 단위로 ISM 을 한 번에 정리하는 세이프가드 추가<ul>
<li>심리스 트래블 직후, <strong>각 클라이언트의 PlayerController가 자신이 속한 월드에서 ISM을 한 번 전체 스캔하여 인스턴스를 비우는 방식</strong>.</li>
</ul>
</li>
</ol>
</li>
</ul>
<ul>
<li>최종 선택<ul>
<li><strong>2번 방식</strong>을 우선 적용:<ul>
<li><code>ACMPlayerController::PostSeamlessTravel()</code> 을 오버라이드.</li>
<li>로컬 컨트롤러 기준으로 월드의 모든 <code>UInstancedStaticMeshComponent</code> 를 순회하며 <code>ClearInstances()</code> 호출.</li>
<li>이 작업 이후, 기존에 구현해둔 <strong>캐릭터 재스폰 + 화면/사운드 페이드 인</strong> 로직을 그대로 이어서 실행.</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="구현-상세">구현 상세</h2>
<h3 id="1-playercontroller-에서-postseamlesstravel-오버라이드">1. PlayerController 에서 PostSeamlessTravel 오버라이드</h3>
<ul>
<li>파일 위치<ul>
<li><code>Source/CrimsonMoon/Public/Controllers/CMPlayerController.h</code></li>
<li><code>Source/CrimsonMoon/Private/Controllers/CMPlayerController.cpp</code></li>
</ul>
</li>
</ul>
<ul>
<li><p>헤더에 오버라이드 선언 (이미 추가되어 있음)</p>
<ul>
<li><p><code>CMPlayerController.h</code> (클래스 내부):</p>
<ul>
<li><code>virtual void PostSeamlessTravel() override;</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>cpp 에 구현 (주요 부분만 발췌)</li>
</ul>
<blockquote>
<p>역할: </p>
<ul>
<li>심리스 트래블 완료 후, <strong>로컬 월드의 모든 ISM 인스턴스를 정리</strong></li>
<li>그 다음, 캐릭터 재스폰 및 화면/사운드 페이드 인</li>
</ul>
</blockquote>
<pre><code class="language-cpp">#include &quot;Components/InstancedStaticMeshComponent.h&quot;
#include &quot;EngineUtils.h&quot; // TActorIterator

void ACMPlayerController::PostSeamlessTravel()
{
    Super::PostSeamlessTravel();

    // 로컬 컨트롤러가 아닌 경우 아무 것도 하지 않음 (서버 전용 PC 등 제외)
    if (!IsLocalController())
    {
        return;
    }

    // 1) 로컬 월드의 모든 InstancedStaticMeshComponent 정리
    if (UWorld* World = GetWorld())
    {
        for (TActorIterator&lt;AActor&gt; It(World); It; ++It)
        {
            AActor* Actor = *It;
            if (!IsValid(Actor))
            {
                continue;
            }

            TArray&lt;UInstancedStaticMeshComponent*&gt; ISMComponents;
            Actor-&gt;GetComponents&lt;UInstancedStaticMeshComponent&gt;(ISMComponents);

            for (UInstancedStaticMeshComponent* ISMComp : ISMComponents)
            {
                if (!ISMComp)
                {
                    continue;
                }

                // 레이트레이싱/충돌 문제를 방지하기 위해 모든 인스턴스를 제거
                ISMComp-&gt;ClearInstances();
                ISMComp-&gt;MarkRenderStateDirty();
            }
        }
    }

    // 2) 심리스 트래블 후 캐릭터 재스폰 요청 (기존 로직)
    NotifyServerPlayerReadyWithCharacter();

    // 3) 화면/사운드 페이드 인 (검정 → 정상, 기존 연출)
    if (PlayerCameraManager)
    {
        PlayerCameraManager-&gt;StartCameraFade(
            /*FromAlpha*/ 1.0f,
            /*ToAlpha*/   0.0f,
            /*Duration*/  1.0f,
            /*Color*/     FLinearColor::Black,
            /*bShouldFadeAudio*/ true,
            /*bHoldWhenFinished*/ false
        );
    }
}</code></pre>
<h3 id="2-기존-연출로직과의-연계">2. 기존 연출/로직과의 연계</h3>
<ul>
<li>이미 <code>ACMPlayerController</code> 에서는 심리스 트래블 이후에 다음 작업을 하고 있었음<ul>
<li><code>NotifyServerPlayerReadyWithCharacter()</code><ul>
<li>서버에 선택된 캐릭터 태그를 보내고, 새 Pawn 스폰 요청.</li>
</ul>
</li>
<li><code>StartCameraFade(1 → 0, bShouldFadeAudio=true)</code><ul>
<li>심리스 트래블 이전에 화면/오디오를 페이드 아웃한 것에 대응하여,</li>
<li>새 맵에서 화면과 소리를 다시 자연스럽게 살리는 연출.</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>이번 수정에서는 이 두 기능을 유지한 채, 그 <strong>이전에 ISM 정리 로직을 끼워 넣는 형태</strong>로 구현.</li>
</ul>
<hr>
<h2 id="적용-결과-및-배운-점">적용 결과 및 배운 점</h2>
<p>이번 수정 이후, 레이트레이싱을 활성화한 빌드에서 심리스 트래블을 반복 테스트했을 때,
기존에 발생하던 <code>FInstancedStaticMeshSceneProxy::GetDynamicRayTracingInstances</code> 관련 Assert 크래시는 더 이상 재현되지 않았습니다.</p>
<p>물론 근본적으로는 <strong>ISM 을 소유하는 각 액터가 자신의 라이프사이클에 맞춰 적절히 정리되도록 설계하는 것</strong>이 가장 좋겠지만,
이번과 같이 레이트레이싱 + 심리스 트래블 조합에서 내부 상태 꼬임으로 인한 크래시가 발생할 때에는,</p>
<ul>
<li>월드 단위로 ISM 을 한 번 &quot;강제로&quot; 정리해 주는 세이프가드를 추가하여</li>
<li>RT/Physics 씬에 남아 있는 잘못된 인스턴스 데이터를 제거하는 것만으로도</li>
<li>실질적인 크래시 문제를 빠르게 완화할 수 있다는 점을 다시 확인하게 되었습니다.</li>
</ul>
<p>앞으로는 ISM 을 대량으로 사용하는 시스템을 설계할 때,</p>
<ul>
<li>멀티플레이/심리스 트래블,</li>
<li>레벨 스트리밍,</li>
<li>레이트레이싱/Physics 씬 빌드 타이밍
같은 요소까지 함께 고려해서 <strong>&quot;누가 언제 ISM 을 생성/정리할 것인가&quot;</strong> 를 처음부터 명확히 정리해 두는 것이 중요하다는 것을 배웠습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] 네트워크 환경에서 플레이어 카메라 Activate 문제 해결]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%B9%B4%EB%A9%94%EB%9D%BC-Activate-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%B9%B4%EB%A9%94%EB%9D%BC-Activate-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 07 Jan 2026 12:30:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_sensational/post/6c9f5fa7-c534-46a0-9baf-79f964759010/image.webp" alt=""></p>
<p>오늘은 언리얼 멀티플레이 환경에서 <strong>플레이어 카메라 시점이 보이지 않는 문제를 Activate로 해결한 과정</strong>과, 아직 남아 있는 <strong>카메라 회전(Input_Look) 문제</strong>를 원인 중심으로 정리하고자 합니다.</p>
<hr>
<h2 id="문제-상황-요약">문제 상황 요약</h2>
<ul>
<li>환경: UE5, 멀티플레이(Host + Client), C++ 기반 캐릭터/컨트롤러, BP 기반 GameplayCamera 사용</li>
<li>증상 1: Client 입장에서 플레이어 Pawn 은 스폰되고 Possess 도 되지만, <strong>카메라 시점이 비어 있거나 이상한 위치를 비춤</strong></li>
<li>증상 2: Host 에서는 정상 동작하는 것처럼 보이지만, <strong>Client 쪽에서만 카메라 관련 문제 발생</strong></li>
<li>추가 증상: 시점을 보이게 만드는 문제는 해결했지만, <strong>마우스 입력에 따른 카메라 상하좌우 회전은 아직 제대로 동작하지 않음</strong></li>
</ul>
<hr>
<h2 id="원인-분석-카메라-시점activate-문제">원인 분석: 카메라 시점(Activate) 문제</h2>
<h3 id="1-viewcamera-vs-gameplaycamera-사용-불일치">1. ViewCamera vs GameplayCamera 사용 불일치</h3>
<ul>
<li>C++ <code>ACMPlayerCharacterBase</code> 안에는 <code>ViewCamera(UCameraComponent)</code> 와 <code>CameraBoom(USpringArmComponent)</code> 가 존재<ul>
<li><code>CameraBoom-&gt;bUsePawnControlRotation = true;</code></li>
<li><code>ViewCamera-&gt;bAutoActivate = false;</code></li>
</ul>
</li>
<li>하지만 실제 게임에서는 캐릭터 BP 에서 <strong>별도의 GameplayCamera 컴포넌트(UGameplayCameraComponent 파생 BP)</strong> 를 메인 카메라로 사용 중</li>
<li>결과적으로:<ul>
<li>C++에서 <code>ViewCamera-&gt;Activate()</code> 를 호출해도 <strong>실제로 화면에 쓰이는 카메라는 GameplayCamera</strong> 이므로, 시점 문제는 그대로 유지</li>
</ul>
</li>
</ul>
<h3 id="2-네트워크에서-possessbeginplay-타이밍-차이">2. 네트워크에서 Possess/BeginPlay 타이밍 차이</h3>
<ul>
<li>서버:<ul>
<li>GameMode 에서 Pawn 생성 → <code>NewPlayer-&gt;Possess(NewPawn)</code> 실행</li>
<li>서버 기준에서는 Possess 타이밍이 확실</li>
</ul>
</li>
<li>클라이언트:<ul>
<li>Pawn 이 복제되고 <code>BeginPlay</code> 가 먼저 호출된 뒤에,</li>
<li>컨트롤러 소유 정보가 들어오고 <code>PawnClientRestart</code>/<code>OnRep_Controller</code> 등이 호출됨</li>
</ul>
</li>
<li><code>BeginPlay</code> 기준으로 카메라 Setup 을 시도하면:<ul>
<li>Host(리스너 서버)는 우연히 잘 동작할 수 있지만,</li>
<li>순수 Client 에서는 <strong>아직 로컬 컨트롤러가 붙지 않은 상태</strong>일 수 있어서 <code>IsLocallyControlled()</code> 가 false 인 경우가 많음</li>
<li>이 경우, 로컬 전용 카메라 Activate 로직이 실행되지 않음</li>
</ul>
</li>
</ul>
<hr>
<h2 id="해결-전략-pawnclientrestart에서-gameplaycamera-activate">해결 전략: PawnClientRestart에서 GameplayCamera Activate</h2>
<h3 id="선택한-기준-시점">선택한 기준 시점</h3>
<ul>
<li>네트워크 플레이에서 <strong>로컬 클라이언트가 이 Pawn 을 실제로 조종할 준비가 된 시점</strong>은 <code>APawn::PawnClientRestart()</code> 에서 가장 확실</li>
<li>이유:<ul>
<li>컨트롤러/Owner 정보가 세팅된 뒤 호출됨</li>
<li>입력 매핑(Enhanced Input)도 이 시점에 다시 셋업하는 패턴이 일반적</li>
<li>Host / Client 양쪽에서 동일한 타이밍 보장</li>
</ul>
</li>
</ul>
<h3 id="구현-아이디어">구현 아이디어</h3>
<ul>
<li><code>ACMPlayerCharacterBase</code> 에 <strong>카메라 셋업 전용 헬퍼 함수</strong> 추가<ul>
<li><code>UGameplayCameraComponent</code> 를 <code>FindComponentByClass</code> 로 찾아온다.</li>
<li>해당 컴포넌트에 대해 <code>Activate()</code> (또는 <code>SetupCamera()</code>) 를 호출한다.</li>
</ul>
</li>
<li><code>PawnClientRestart()</code> 에서:<ul>
<li>기존 입력 매핑 설정 후</li>
<li>바로 이 헬퍼 함수를 호출하여, 로컬 플레이어의 카메라를 활성화한다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="적용한-헬퍼-함수-소스코드">적용한 헬퍼 함수 소스코드</h2>
<h3 id="1-헤더-선언-acmplayercharacterbaseh">1. 헤더 선언 (ACMPlayerCharacterBase.h)</h3>
<ul>
<li>전방 선언 및 private 헬퍼 함수 선언</li>
</ul>
<pre><code class="language-cpp">class UGameplayCameraComponent;

UCLASS()
class CRIMSONMOON_API ACMPlayerCharacterBase : public ACMCharacterBase
{
    GENERATED_BODY()

    // ...existing code...

private:
    // BeginPlay 이후, 로컬 플레이어 기준으로 GameplayCamera를 셋업하는 헬퍼 함수
    void SetupGameplayCamera_Helper();

    // ...existing code...
};</code></pre>
<h3 id="2-구현부-acmplayercharacterbasecpp">2. 구현부 (ACMPlayerCharacterBase.cpp)</h3>
<pre><code class="language-cpp">void ACMPlayerCharacterBase::SetupGameplayCamera_Helper()
{
    UE_LOG(LogTemp, Warning,
        TEXT(&quot;SetupGameplayCamera_Helper: Called. IsLocallyControlled=%d&quot;),
        IsLocallyControlled() ? 1 : 0);

    // 로컬 플레이어가 소유한 Pawn 에서만 카메라 셋업
    if (!IsLocallyControlled())
    {
        UE_LOG(LogTemp, Warning,
            TEXT(&quot;SetupGameplayCamera_Helper: Aborted - not locally controlled&quot;));
        return;
    }

    // BP 에서 추가된 UGameplayCameraComponent 를 찾아 SetupCamera/Activate 호출
    if (UGameplayCameraComponent* GameplayCameraComp = FindComponentByClass&lt;UGameplayCameraComponent&gt;())
    {
        UE_LOG(LogTemp, Warning,
            TEXT(&quot;SetupGameplayCamera_Helper: Found GameplayCameraComponent on %s, calling SetupCamera&quot;),
            *GetName());

        // 현재는 Activate로 시점을 살려두었음 (필요 시 SetupCamera()로 교체 가능)
        GameplayCameraComp-&gt;Activate();
        // GameplayCameraComp-&gt;SetupCamera();
    }
    else
    {
        UE_LOG(LogTemp, Warning,
            TEXT(&quot;SetupGameplayCamera_Helper: GameplayCameraComponent not found on %s&quot;),
            *GetName());
    }
}</code></pre>
<h4 id="pawnclientrestart에서-호출">PawnClientRestart에서 호출</h4>
<pre><code class="language-cpp">void ACMPlayerCharacterBase::PawnClientRestart()
{
    Super::PawnClientRestart();

    if (const APlayerController* OwningPlayerController = GetController&lt;APlayerController&gt;())
    {
        UEnhancedInputLocalPlayerSubsystem* PlayerSubsystem =
            OwningPlayerController-&gt;GetLocalPlayer()-&gt;GetSubsystem&lt;UEnhancedInputLocalPlayerSubsystem&gt;();

        check(PlayerSubsystem);

        PlayerSubsystem-&gt;RemoveMappingContext(InputConfigDataAsset-&gt;DefaultMappingContext);
        PlayerSubsystem-&gt;AddMappingContext(InputConfigDataAsset-&gt;DefaultMappingContext, 0);
    }

    // 로컬 클라이언트가 이 Pawn 을 다시 사용할 준비가 된 시점에 카메라 셋업 시도
    SetupGameplayCamera_Helper();
}</code></pre>
<hr>
<h2 id="현재-상태-시점-문제는-해결-회전-문제는-미해결">현재 상태: 시점 문제는 해결, 회전 문제는 미해결</h2>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/19725d02-0b07-403c-b24d-382cdcf5fc10/image.webp" alt=""></p>
<h3 id="해결된-부분">해결된 부분</h3>
<ul>
<li>Host / Client 모두에서:<ul>
<li>GameplayCameraComponent 를 <strong>로컬 클라이언트 기준으로 Activate</strong> 하도록 변경</li>
<li>PawnClientRestart 기준으로 실행되기 때문에, 네트워크 타이밍 문제(컨트롤러 미소유 상태)는 피함</li>
</ul>
</li>
<li>결과적으로:<ul>
<li><strong>카메라 시점이 비어 있거나, 이상한 곳을 비추던 문제는 해결</strong>됨</li>
<li>캐릭터를 정상적으로 화면에 비추고, 이동/전투 플레이가 가능해짐</li>
</ul>
</li>
</ul>
<h3 id="아직-해결되지-않은-부분-마우스-입력에-따른-카메라-회전">아직 해결되지 않은 부분: 마우스 입력에 따른 카메라 회전</h3>
<ul>
<li>남아 있는 문제:<ul>
<li>마우스 입력(<code>Input_Look</code>)에 따라 카메라가 <strong>상하좌우로 자연스럽게 회전해야 하는데</strong>,</li>
<li>지금은 시점은 잡히지만, <strong>회전이 작동하지 않음</strong></li>
</ul>
</li>
<li>추정 원인:<ul>
<li>C++ 측에서는 <code>Input_Look</code> 에서 <code>AddControllerYawInput</code>, <code>AddControllerPitchInput</code> 을 호출하고 있음</li>
<li>하지만 실제로 화면에 쓰이는 것은 <strong>BP 기반 GameplayCamera</strong> 이고,</li>
<li>이 GameplayCamera/SpringArm 이 컨트롤러 회전을 제대로 반영하지 않거나,<ul>
<li><code>bUsePawnControlRotation</code> 설정이 비활성화되어 있거나,</li>
<li>자체 로직으로만 회전을 제어하고 있을 가능성이 큼</li>
</ul>
</li>
<li>따라서 <strong>컨트롤러 회전 값은 변하지만, 카메라 컴포넌트가 그 값을 사용하지 않는 상태</strong>일 수 있음</li>
</ul>
</li>
<li>요약:<ul>
<li>&quot;카메라가 안 보인다&quot; 문제는 <code>UGameplayCameraComponent</code> Activate 로 해결</li>
<li>&quot;카메라가 마우스 입력대로 돌지 않는다&quot; 문제는 <strong>GameplayCamera의 회전 설정/로직 조정</strong>이 추가로 필요</li>
</ul>
</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>이번 작업에서는 네트워크 환경에서 플레이어 카메라 시점이 비정상적으로 비춰지는 문제를, <code>PawnClientRestart</code> 시점에 <code>UGameplayCameraComponent</code> 를 찾아 Activate 하는 방식으로 해결하였습니다. 특히 BeginPlay, PossessedBy, PawnClientRestart 간의 호출 시점과 <code>IsLocallyControlled()</code> 여부가 네트워크 환경에서 어떻게 달라지는지 다시 한 번 정리할 수 있었고, 로컬 전용 카메라 로직은 PawnClientRestart 에서 처리하는 것이 안전하다는 점을 확인했습니다.</p>
<p>다만, 아직 마우스 입력에 따른 카메라 상하좌우 회전이 기대한 대로 동작하지 않는 문제가 남아 있습니다. 이는 C++ 단의 Input_Look 처리가 아니라, 실제 뷰를 담당하고 있는 GameplayCamera 컴포넌트의 회전 설정과 로직이 컨트롤러 회전을 어떻게 받아들이는지에 더 가깝기 때문에, 다음 단계에서는 해당 BP/컴포넌트의 <code>bUsePawnControlRotation</code> 설정, SpringArm 사용 여부, ViewTarget 세팅 방식을 집중적으로 점검하고 수정할 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] GameStarter NPC 구현 및 Remote Client 필터링 문제 해결]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-GameStarter-NPC-%EA%B5%AC%ED%98%84-%EB%B0%8F-Remote-Client-%ED%95%84%ED%84%B0%EB%A7%81-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-GameStarter-NPC-%EA%B5%AC%ED%98%84-%EB%B0%8F-Remote-Client-%ED%95%84%ED%84%B0%EB%A7%81-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 06 Jan 2026 10:29:33 GMT</pubDate>
            <description><![CDATA[<p>이번에는 멀티플레이 환경에서 <strong>게임 시작용 NPC(GameStarter NPC)</strong> 를 구현하고, 발생한 문제를 해결했습니다.</p>
<p>목표는 다음과 같습니다.</p>
<blockquote>
</blockquote>
<ul>
<li>NPC와 상호작용(Interact)했을 때 <strong>서버 인원 전체</strong>를 특정 맵으로 이동시키기</li>
<li>이동 전 <strong>화면이 서서히 어두워지는 연출(페이드 아웃)</strong> 추가</li>
<li>Interact 로직이 <strong>항상 서버(Host)</strong> 에서만 실행되도록 보장</li>
<li>특히, <strong>Remote Client가 조종하는 캐릭터가 Interact를 수행했을 때는 GameStarter 로직이 실행되지 않도록 필터링</strong></li>
</ul>
<p>특히, 게임 시작이 Host에서만 수행되도록 해야 하지만, Interact가 서버를 통해 수행되어 의도와 다르게 수행됐던 문제를 중점적으로 다뤄보고자 합니다.</p>
<hr>
<h2 id="현재-상호작용interact-구조-분석">현재 상호작용(Interact) 구조 분석</h2>
<h3 id="1-입력-처리-흐름-클라이언트">1. 입력 처리 흐름 (클라이언트)</h3>
<ul>
<li>컴포넌트: <code>UCMPickUpComponent</code></li>
<li>역할: 입력 바인딩 및 상호작용 대상 관리</li>
<li>주요 특징<ul>
<li><code>BeginPlay</code>에서 입력 시스템 초기화 시도 (<code>InitializeInputSystem</code>)</li>
<li>로컬 플레이어 컨트롤러가 준비되면 Enhanced Input으로 Interact 액션 바인딩</li>
<li>Tick에서 서버 기준으로 가장 가까운 상호작용 대상(<code>CurrentInteractableActor</code>)을 계산 및 복제</li>
</ul>
</li>
</ul>
<ul>
<li>입력 처리 함수<ul>
<li><code>UCMPickUpComponent::OnPickupInput(const FInputActionValue&amp; Value)</code><ul>
<li>Interact 키(예: <code>2번</code>)를 누르면 호출</li>
<li>직접 트레이스를 하지 않고, <strong>Ability System에 태그를 기반으로 Activiate 요청</strong></li>
<li>코드 요약<ul>
<li><code>AbilitySystemComponent-&gt;TryActivateAbilitiesByTag(InteractAbilityTag, true);</code></li>
</ul>
</li>
<li>이 부분은 <strong>클라이언트 로컬에서만 실행</strong>됨</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="2-gameplay-ability-실행-서버">2. Gameplay Ability 실행 (서버)</h3>
<ul>
<li>클래스: <code>UCMAbility_Interact</code></li>
<li>역할: Interact Ability의 실제 실행 로직</li>
<li>핵심 설정<ul>
<li><code>NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerOnly;</code></li>
<li>의미: Ability의 <code>ActivateAbility</code> 는 <strong>항상 서버에서만 실행</strong></li>
</ul>
</li>
</ul>
<ul>
<li>실행 흐름 요약<ul>
<li><code>AvatarActor</code> (보통 플레이어 캐릭터)를 가져옴</li>
<li><code>AvatarActor</code>에서 <code>UCMPickUpComponent</code>를 찾음</li>
<li><code>PickUpComp-&gt;GetCurrentInteractable()</code>로 상호작용 대상 <code>HitActor</code> 획득</li>
<li><code>HitActor</code>가 <code>UCMInteractableInterface</code> 구현 시:<ul>
<li><code>ICMInteractableInterface::Execute_Interact(HitActor, AvatarActor);</code></li>
</ul>
</li>
<li>어빌리티 종료 (<code>EndAbility</code>)</li>
</ul>
</li>
</ul>
<ul>
<li>시사점<ul>
<li>Interact 입력은 <strong>클라이언트에서 시작</strong>하지만,</li>
<li>실제 Interact 실행(인터페이스 호출)은 <strong>서버에서 수행</strong>됨</li>
</ul>
</li>
</ul>
<h3 id="3-npc-인터페이스-및-gamestarter-npc-역할">3. NPC 인터페이스 및 GameStarter NPC 역할</h3>
<ul>
<li>기본 NPC 클래스: <code>ACMNpcBase</code><ul>
<li><code>ICMInteractableInterface</code> 를 구현</li>
<li>기본적인 상호작용/대화/상점 등 공통 로직 보유</li>
</ul>
</li>
</ul>
<ul>
<li>GameStarter NPC: <code>ACMNpcGameStarter : public ACMNpcBase</code><ul>
<li>역할<ul>
<li>특정 NPC와의 Interact 시, <strong>서버 전체를 지정 맵으로 이동(ServerTravel)</strong></li>
<li>이동 직전, 모든 클라이언트 화면을 <strong>서서히 어둡게(페이드 아웃)</strong> 처리</li>
</ul>
</li>
<li>제약 조건<ul>
<li><strong>Ability 쪽 코드는 수정하지 않음</strong></li>
<li>필터링 로직은 <strong>GameStarter NPC 내부에서만 구현</strong></li>
<li>Remote Client가 조종하는 캐릭터의 Interact는 <strong>무시</strong></li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="gamestarter-npc에서의-remote-client-필터링-설계">GameStarter NPC에서의 Remote Client 필터링 설계</h2>
<h3 id="1-요구사항-정리">1. 요구사항 정리</h3>
<ul>
<li>Interact 구조(입력 → Ability → 서버에서 Interact 호출)는 그대로 유지</li>
<li>단, GameStarter NPC 입장에서 <strong>Interactor가 누구인지 보고</strong> 다음과 같이 처리<ul>
<li>Interactor가 <strong>Remote 클라이언트가 조종하는 Pawn</strong>이면 → GameStarter 로직 실행 X</li>
<li>Interactor가 <strong>리슨 서버(Host)가 조종하는 Pawn</strong> 또는 기타 허용된 주체라면 → GameStarter 로직 실행 O</li>
</ul>
</li>
</ul>
<h3 id="2-서버-관점에서-remote-client-판별-기준">2. 서버 관점에서 Remote Client 판별 기준</h3>
<ul>
<li>서버 기준에서 <code>APawn</code> 의 <code>Controller</code> 를 통해 판별</li>
<li>판별 로직<ul>
<li><code>Controller-&gt;IsPlayerController() == true</code> 이면, 플레이어가 조종하는 Pawn</li>
<li>이 때,<ul>
<li><code>Controller-&gt;IsLocalController() == true</code>  → 서버 자신(리슨 서버) 혹은 서버 로컬 컨트롤러</li>
<li><code>Controller-&gt;IsLocalController() == false</code> → <strong>Remote 클라이언트가 조종 중인 Pawn</strong></li>
</ul>
</li>
</ul>
</li>
<li>따라서 아래 조합으로 Remote 클라이언트를 판별 가능<ul>
<li><code>Controller-&gt;IsPlayerController()</code> &amp;&amp; <code>!Controller-&gt;IsLocalController()</code></li>
</ul>
</li>
</ul>
<p>이를 GameStarter NPC의 <code>Interact_Implementation</code> 안에서 사용하여 필터링을 구현했습니다.</p>
<hr>
<h2 id="acmnpcgamestarter-내부-구현-변화">ACMNpcGameStarter 내부 구현 변화</h2>
<h3 id="1-클래스-인터페이스-확장-헤더">1. 클래스 인터페이스 확장 (헤더)</h3>
<ul>
<li><p>파일: <code>CMNpcGameStarter.h</code></p>
</li>
<li><p>변경 사항</p>
<ul>
<li><code>Interact_Implementation(AActor* Interactor)</code> 오버라이드 선언</li>
<li><code>PerformInteract()</code> 를 <code>override</code> 로 선언해 GameStarter 전용 로직 수행</li>
<li>GameStart에 필요한 설정값을 프로퍼티로 추가</li>
</ul>
</li>
<li><p>주요 멤버 요약</p>
<ul>
<li><code>virtual void Interact_Implementation(AActor* Interactor) override;</code></li>
<li><code>virtual void PerformInteract() override;</code></li>
<li><code>UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;GameStart&quot;) FString TravelURL;</code></li>
<li><code>UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = &quot;GameStart&quot;) float FadeDuration = 1.0f;</code></li>
<li><code>UFUNCTION(NetMulticast, Reliable) void MulticastStartFadeOut();</code></li>
<li><code>void ServerTravelToConfiguredMap();</code></li>
</ul>
</li>
</ul>
<h3 id="2-interact_implementation에서-remote-client-필터링">2. Interact_Implementation에서 Remote Client 필터링</h3>
<ul>
<li>파일: <code>CMNpcGameStarter.cpp</code></li>
<li>핵심 로직<ul>
<li>오직 <strong>서버(또는 리슨 서버)</strong> 에서만 Interact 처리</li>
<li><code>Interactor</code> 가 Remote 클라가 조종하는 Pawn이면 조용히 <code>return;</code></li>
<li>그 외 Interactor에 대해서만 <code>PerformInteract()</code> 호출</li>
</ul>
</li>
<li>로직 개요<ul>
<li>권한 체크<ul>
<li><code>if (!HasAuthority() &amp;&amp; GetNetMode() != NM_ListenServer) return;</code></li>
</ul>
</li>
<li>Interactor 타입 검사<ul>
<li><code>APawn* Pawn = Cast&lt;APawn&gt;(Interactor);</code></li>
<li><code>AController* Controller = Pawn-&gt;GetController();</code></li>
</ul>
</li>
<li>Remote Client 판별<ul>
<li><code>Controller-&gt;IsPlayerController() &amp;&amp; !Controller-&gt;IsLocalController()</code> → Remote 클라</li>
</ul>
</li>
<li>필터링<ul>
<li>위 조건이면 <code>return;</code> → GameStart 로직 미실행</li>
</ul>
</li>
<li>허용 시<ul>
<li><code>PerformInteract();</code> 호출</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="3-performinteract-서버-전용-gamestart-로직">3. PerformInteract: 서버 전용 GameStart 로직</h3>
<ul>
<li>역할<ul>
<li>서버에서만 실행되는 GameStart 핵심 로직</li>
<li>전체 클라이언트에 페이드 아웃 명령 전파</li>
<li>페이드가 끝난 시점에 ServerTravel 수행</li>
</ul>
</li>
<li>동작 순서<ol>
<li><code>HasAuthority()</code> + <code>NetMode</code> 체크로 서버/리슨 서버에서만 진행</li>
<li><code>UWorld* World = GetWorld();</code> 유효성 확인</li>
<li><code>MulticastStartFadeOut();</code> 호출 → NetMulticast RPC</li>
<li><code>World-&gt;GetTimerManager().SetTimer(..., FadeDuration);</code> 로 <code>ServerTravelToConfiguredMap</code> 예약 호출</li>
</ol>
</li>
<li>이로 인해:<ul>
<li>클라이언트가 Interact 입력을 하더라도</li>
<li>Ability → 서버에서 Interact → GameStarter → <strong>추가 필터링 후 허용된 경우에만</strong> 서버 트래블이 실행됨</li>
</ul>
</li>
</ul>
<h3 id="4-multicaststartfadeout-모든-클라에서-로컬-화면-페이드">4. MulticastStartFadeOut: 모든 클라에서 로컬 화면 페이드</h3>
<ul>
<li>함수: <code>MulticastStartFadeOut_Implementation()</code></li>
<li>특성: <code>NetMulticast, Reliable</code><ul>
<li>서버에서 한 번 호출하면, <strong>서버 + 모든 클라이언트 월드에서 각각 1번씩 실행</strong></li>
</ul>
</li>
<li>동작<ul>
<li><code>UWorld* World = GetWorld();</code></li>
<li><code>APlayerController* PC = World-&gt;GetFirstPlayerController();</code></li>
<li><code>PC-&gt;IsLocalController()</code> 인 경우에만 페이드 적용</li>
<li><code>PC-&gt;PlayerCameraManager-&gt;StartCameraFade(0.0f, 1.0f, FadeDuration, FLinearColor::Black, true, true);</code></li>
</ul>
</li>
<li>결과<ul>
<li>각 클라이언트가 <strong>자기 화면에서만</strong> 0 → 1 알파로 서서히 어두워짐</li>
<li>Remote/로컬 구분 없이, 모든 접속자 화면에서 동일한 페이드 아웃 연출 발생</li>
</ul>
</li>
</ul>
<h3 id="5-servertraveltoconfiguredmap-서버-인원-전체-맵-이동">5. ServerTravelToConfiguredMap: 서버 인원 전체 맵 이동</h3>
<ul>
<li>역할<ul>
<li>에디터에서 설정한 <code>TravelURL</code> 을 기준으로 ServerTravel 실행</li>
</ul>
</li>
<li>동작 요약<ul>
<li>서버/리슨 서버에서만 실행 (<code>HasAuthority()</code> 체크)</li>
<li><code>TravelURL.IsEmpty()</code> 시 로그만 출력하고 종료</li>
<li><code>World-&gt;ServerTravel(TravelURL, /*bAbsolute*/ false);</code></li>
</ul>
</li>
<li>효과<ul>
<li>서버가 새로운 맵으로 트래블하면서,</li>
<li>현재 접속 중인 모든 클라이언트도 해당 맵으로 함께 이동</li>
</ul>
</li>
</ul>
<hr>
<h2 id="5-상호작용-흐름-전체-시각화">5. 상호작용 흐름 전체 시각화</h2>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/1b372dfb-8ac8-4c41-b5a9-b074674b2c59/image.png" alt=""></p>
<hr>
<h2 id="6-얻은-인사이트-및-정리">6. 얻은 인사이트 및 정리</h2>
<p>오늘 작업을 통해 다음과 같은 점을 다시 정리할 수 있었습니다.</p>
<ul>
<li><strong>입력과 실행 주체의 분리</strong><ul>
<li>상호작용 입력은 클라이언트에서 발생하지만,</li>
<li>실제 게임 규칙(Logics)은 ServerOnly Ability와 서버 측 NPC 로직을 통해 <strong>서버에서만 결정</strong>하도록 설계할 수 있음</li>
</ul>
</li>
</ul>
<ul>
<li><strong>Remote Client 필터링은 Interactor 정보를 활용해 GameStart 클래스 내부에서 충분히 처리 가능</strong><ul>
<li>Ability 구조를 건드리지 않고도,</li>
<li><code>Interact_Implementation(AActor* Interactor)</code> 에서 Interactor의 Controller를 검사하여</li>
<li>Remote 클라이언트가 조종하는 Pawn의 Interact만 깔끔하게 차단할 수 있었음</li>
</ul>
</li>
</ul>
<ul>
<li><strong>NetMulticast + 로컬 컨트롤러 검사로 시각 효과를 안전하게 분배</strong><ul>
<li><code>NetMulticast</code> 함수는 월드마다 한 번씩 실행되므로,</li>
<li>함수 내부에서 <code>GetFirstPlayerController()</code> + <code>IsLocalController()</code> 조합을 사용하면</li>
<li>각 클라이언트의 로컬 화면에만 시각 효과(페이드)를 적용할 수 있음</li>
</ul>
</li>
</ul>
<ul>
<li><strong>서버 트래블(ServerTravel)과 연출(페이드)을 분리하여 타이밍 제어</strong><ul>
<li>페이드 아웃 → 타이머 → ServerTravel 순으로 분리함으로써,</li>
<li>연출 타이밍과 실제 맵 전환 타이밍을 명확하게 제어할 수 있었음</li>
</ul>
</li>
</ul>
<p>앞으로는 이 패턴을 바탕으로, 특정 권한(예: GM 전용, 특정 팀 전용)만 사용할 수 있는 상호작용 오브젝트를 설계할 때, Interactor 기반 필터링을 적극적으로 활용할 계획입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] 상점 기능 고도화 (컨텐츠 정보 반영, 개수 증감 버튼 등)]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-%EC%83%81%EC%A0%90-%EA%B8%B0%EB%8A%A5-%EA%B3%A0%EB%8F%84%ED%99%94-%EC%BB%A8%ED%85%90%EC%B8%A0-%EC%A0%95%EB%B3%B4-%EB%B0%98%EC%98%81-%EA%B0%9C%EC%88%98-%EC%A6%9D%EA%B0%90-%EB%B2%84%ED%8A%BC-%EB%93%B1</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-%EC%83%81%EC%A0%90-%EA%B8%B0%EB%8A%A5-%EA%B3%A0%EB%8F%84%ED%99%94-%EC%BB%A8%ED%85%90%EC%B8%A0-%EC%A0%95%EB%B3%B4-%EB%B0%98%EC%98%81-%EA%B0%9C%EC%88%98-%EC%A6%9D%EA%B0%90-%EB%B2%84%ED%8A%BC-%EB%93%B1</guid>
            <pubDate>Tue, 30 Dec 2025 11:31:07 GMT</pubDate>
            <description><![CDATA[<p>오늘은 프로젝트에서 Shop UI를 구현하고, 특히 <strong>상점 리스트에서 아이템을 선택했을 때 상세 패널에 정보가 반영되는 흐름</strong>과 <strong>아이템 수량(Quantity) 증감 UI</strong>를 중심으로 작업을 진행하였습니다. 또한 상점 UI를 여는 과정에서 <strong>PlayerController–NPC–ShopComponent</strong> 사이의 통신 구조를 다시 정리하고, 이를 코드 레벨에서 점검하는 시간을 가졌습니다.</p>
<p>이번 작업의 핵심은 다음과 같습니다. <strong>CMShopContentElementWidget을 클릭 → CMShopWidget에서 선택 상태 갱신 → 상세 UI에 이름/설명/수량 표시 → 수량 버튼으로 Quantity 증감</strong>까지의 흐름을 자연스럽게 만드는 것이었습니다.</p>
<hr>
<h2 id="오늘-구현정리한-내용">오늘 구현/정리한 내용</h2>
<h3 id="shop-ui-전체-흐름-요약">Shop UI 전체 흐름 요약</h3>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/69f725e2-b153-4e5f-9e1e-475326017a08/image.png" alt=""></p>
<ul>
<li>서버에서 상점 데이터를 가져와서 <strong>클라이언트의 UCMShopWidget</strong>에 전달하는 기존 구조를 재확인하였습니다.</li>
<li><code>CreateShopWidget</code>에서 이미 상점 위젯 인스턴스가 있을 경우에는 <strong>SetShopItems + BuildShopList</strong>만 다시 호출하여 재사용하도록 되어 있음을 확인했습니다.</li>
</ul>
<hr>
<h3 id="cmshopwidget-아이템-선택-및-수량-표시-로직">CMShopWidget: 아이템 선택 및 수량 표시 로직</h3>
<h4 id="주요-멤버-정리">주요 멤버 정리</h4>
<ul>
<li><code>FCMShopItemContent CurrentSelectedItem;</code><ul>
<li>현재 선택된 상점 아이템 정보 구조체</li>
</ul>
</li>
<li><code>int32 SeletedItemQuantity = 0;</code><ul>
<li>현재 선택된 아이템의 수량(Quantity)</li>
</ul>
</li>
<li><code>UTextBlock* SelectedItemNameText;</code><ul>
<li>선택 아이템 이름 표시용 텍스트</li>
</ul>
</li>
<li><code>UTextBlock* SelectedItemQuantityText;</code><ul>
<li>선택 아이템 수량 표시용 텍스트</li>
</ul>
</li>
<li><code>UTextBlock* ItemDescriptionText;</code><ul>
<li>선택 아이템 설명 표시용 텍스트</li>
</ul>
</li>
<li><code>UButton* AddItemQuantityButton;</code><ul>
<li>수량 증가 버튼(+)</li>
</ul>
</li>
<li><code>UButton* SubtractItemQuantityButton;</code><ul>
<li>수량 감소 버튼(-)</li>
</ul>
</li>
</ul>
<h4 id="초기화-및-버튼-바인딩">초기화 및 버튼 바인딩</h4>
<ul>
<li><code>NativeOnInitialized()</code>에서 다음과 같이 버튼과 핸들러를 바인딩<ul>
<li><code>AddItemQuantityButton -&gt; OnClickedAddItemQuantityButton</code></li>
<li><code>SubtractItemQuantityButton -&gt; OnClickedSubtractItemQuantityButton</code></li>
</ul>
</li>
<li>이 핸들러 함수들은 반드시 <code>UFUNCTION()</code>으로 선언해 델리게이트와 호환되도록 처리</li>
</ul>
<h4 id="아이템-선택-처리-handleelementselected">아이템 선택 처리: HandleElementSelected</h4>
<p><code>UCMShopContentElementWidget</code>(리스트 요소)을 클릭했을 때 호출되는 콜백입니다.</p>
<ul>
<li>역할<ul>
<li>리스트에서 클릭된 요소의 <code>FCMShopItemContent</code>를 받아와 <strong>현재 선택 아이템</strong>으로 저장</li>
<li>선택과 동시에 수량을 <strong>1로 초기화</strong></li>
<li>상세 UI 텍스트를 갱신</li>
</ul>
</li>
</ul>
<p>개념적으로는 다음과 같습니다.</p>
<ul>
<li><code>CurrentSelectedItem = ElementWidget-&gt;GetItemContent();</code></li>
<li><code>SeletedItemQuantity = 1;</code></li>
<li><code>UpdateSelectedItemDisplay();</code></li>
</ul>
<h4 id="상세-표시-함수-updateselecteditemdisplay">상세 표시 함수: UpdateSelectedItemDisplay</h4>
<p>선택된 아이템 정보를 실제 UI 텍스트에 반영하는 책임을 가집니다.</p>
<ul>
<li>이름 텍스트<ul>
<li><code>SelectedItemNameText-&gt;SetText(...)</code> 호출</li>
<li><code>FCMShopItemContent</code> 내부의 이름 필드 타입을 고려하여 <code>FName</code> → <code>FText::FromName</code>, 또는 이미 <code>FText</code>라면 그대로 세팅</li>
</ul>
</li>
<li>수량 텍스트<ul>
<li><code>SelectedItemQuantityText-&gt;SetText(FText::AsNumber(SeletedItemQuantity));</code></li>
</ul>
</li>
<li>설명 텍스트<ul>
<li><code>ItemDescriptionText-&gt;SetText(CurrentSelectedItem.ItemDescription);</code></li>
</ul>
</li>
</ul>
<p>에디터에서 바인딩한 텍스트 위젯들이 <code>nullptr</code>일 수 있으므로, 각 항목마다 <code>if (SelectedItemNameText)</code> 같은 <strong>널 체크 후 세팅</strong>하는 패턴으로 작성했습니다.</p>
<h4 id="수량-증감-로직-updateselecteditemquantitydelta">수량 증감 로직: UpdateSelectedItemQuantityDelta</h4>
<p>수량 버튼 양쪽에서 공통으로 사용하는 내부 함수입니다.</p>
<ul>
<li>인자<ul>
<li><code>int32 Delta</code> : +1, -1 등의 증감 값</li>
</ul>
</li>
<li>처리<ul>
<li><code>SeletedItemQuantity = FMath::Clamp(SeletedItemQuantity + Delta, 1, 99);</code><ul>
<li>최소 1, 최대 99로 제한</li>
</ul>
</li>
<li><code>UpdateSelectedItemDisplay();</code> 호출하여 UI를 동기화</li>
</ul>
</li>
</ul>
<p>이렇게 함으로써, 수량이 UI와 항상 <strong>동일한 상태</strong>를 유지하게 했습니다.</p>
<h4 id="수량-버튼-클릭-핸들러">수량 버튼 클릭 핸들러</h4>
<ul>
<li><code>OnClickedAddItemQuantityButton()</code><ul>
<li><code>UpdateSelectedItemQuantityDelta(1);</code></li>
</ul>
</li>
<li><code>OnClickedSubtractItemQuantityButton()</code><ul>
<li><code>UpdateSelectedItemQuantityDelta(-1);</code></li>
</ul>
</li>
</ul>
<p>버튼은 단순히 <strong>증감 방향만 결정</strong>하고, 실제 로직은 <code>UpdateSelectedItemQuantityDelta</code>에 몰아 넣어 중복을 줄였습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/61e6cc62-0935-44ee-a68e-47d355ded6ef/image.png" alt=""></p>
<hr>
<h3 id="cmshopcontentelementwidget과-cmshopwidget의-연결-방식">CMShopContentElementWidget과 CMShopWidget의 연결 방식</h3>
<p><code>UCMShopContentElementWidget</code>은 상점 리스트의 개별 아이템 슬롯입니다. 오늘은 이 위젯이 <strong>어떻게 상위 ShopWidget에 &quot;선택됨&quot;을 알리는지</strong> 흐름을 명확히 이해하는 데 집중했습니다.</p>
<h4 id="데이터-전달-구조-정리">데이터 전달 구조 (정리)</h4>
<ul>
<li><code>CMNpcShopComponent</code>에서 <code>TArray&lt;FCMShopItemContent&gt;</code>를 생성 및 관리</li>
<li><code>ACMPlayerController::Server_RequestShopData_Implementation</code><ul>
<li>NPC를 찾고, 그 NPC의 <code>UCMNpcShopComponent</code>에서 <code>GetShopItemContents</code>로 아이템 배열 획득</li>
<li>ListenServer/클라 구분 후, 최종적으로 <code>CreateShopWidget(ShopItems)</code> 호출</li>
</ul>
</li>
<li><code>UCMShopWidget::BuildShopList()</code><ul>
<li><code>ShopItems</code>를 순회하면서 <code>UCMShopContentElementWidget</code> 인스턴스들을 생성</li>
<li>각 슬롯에 <code>SetItemDisplayData(Item, this)</code>와 같은 방식으로<ul>
<li>표시용 데이터(이름, 가격, 아이콘 등)</li>
<li>콜백 대상(ShopWidget 자기 자신)을 전달</li>
</ul>
</li>
</ul>
</li>
<li><code>UCMShopContentElementWidget</code> 내부에서 OnClicked 등 이벤트 발생 시<ul>
<li>바인딩된 <code>UCMShopWidget::HandleElementSelected(this)</code> 호출</li>
</ul>
</li>
</ul>
<p>이 과정을 통해 <strong>슬롯 → 상점 메인 위젯</strong>으로 자연스럽게 선택 이벤트가 올라오도록 설계되어 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/86e84f1d-6b6f-447f-bf8e-1fd3745587ba/image.png" alt=""></p>
<hr>
<h3 id="네이밍-및-바인딩-정리-count-→-quantity">네이밍 및 바인딩 정리 (Count → Quantity)</h3>
<p>오늘 도중에 발견한 네이밍 혼선을 정리했습니다. 기존에는 일부 버튼/텍스트가 <code>Count</code>라는 이름을 사용하고 있었고, 다른 부분에서는 <code>Quantity</code>를 사용하고 있었습니다. 이를 전부 <code>Quantity</code>로 통일했습니다.</p>
<ul>
<li>버튼<ul>
<li><code>AddItemCountButton</code> → <code>AddItemQuantityButton</code></li>
<li><code>SubtractItemCountButton</code> → <code>SubtractItemQuantityButton</code></li>
</ul>
</li>
<li>텍스트<ul>
<li><code>SelectedItemCountText</code> → <code>SelectedItemQuantityText</code></li>
</ul>
</li>
<li>함수<ul>
<li><code>OnClickedAddItemCountButton</code> → <code>OnClickedAddItemQuantityButton</code></li>
<li><code>OnClickedSubtractItemCountButton</code> → <code>OnClickedSubtractItemQuantityButton</code></li>
</ul>
</li>
<li>UI 바인딩 주의<ul>
<li>C++ 이름을 변경한 후, UMG 디자이너에서 <strong>위젯 변수 이름도 동일하게 변경</strong>해야 바인딩이 끊기지 않음을 재확인했습니다.</li>
</ul>
</li>
</ul>
<p>이 정리 덕분에 코드 가독성과 의도가 보다 명확해졌습니다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>오늘은 Shop UI에서 <strong>아이템 선택 → 상세 정보 반영 → 수량 증감</strong>이라는 한 흐름에 집중해서 구현과 리팩터링을 진행하였습니다. 특히 <code>CMShopContentElementWidget</code>과 <code>CMShopWidget</code> 사이의 이벤트 전달 구조를 확실히 이해하고 정리하면서, 이후 구매/판매 요청(<code>RequestBuyItem</code>, <code>RequestSellItem</code>)을 구현할 때도 같은 패턴을 확장해서 사용할 수 있겠다는 생각이 들었습니다.</p>
<p>또한, 이름을 <code>Count</code>에서 <code>Quantity</code>로 통일하고 버튼/텍스트/함수를 일관되게 정리한 덕분에, 나중에 상점 관련 기능을 확장할 때 혼동이 줄어들 것으로 기대합니다. 다음 단계에서는 오늘 만든 선택/수량 정보를 바탕으로 실제 서버 RPC(<code>Server_RequestBuyItem</code>, <code>Server_RequestSellItem</code>)와 연동하여 아이템 구매/판매 로직을 완성해 볼 계획입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] 서버 권한 상점 컨텐츠 구성 (UI, 캐싱, Data Table, RPC)]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-%EC%84%9C%EB%B2%84-%EA%B6%8C%ED%95%9C-%EC%83%81%EC%A0%90-%EC%BB%A8%ED%85%90%EC%B8%A0-%EA%B5%AC%EC%84%B1-UI-%EC%BA%90%EC%8B%B1-Data-Table-RPC</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-%EC%84%9C%EB%B2%84-%EA%B6%8C%ED%95%9C-%EC%83%81%EC%A0%90-%EC%BB%A8%ED%85%90%EC%B8%A0-%EA%B5%AC%EC%84%B1-UI-%EC%BA%90%EC%8B%B1-Data-Table-RPC</guid>
            <pubDate>Fri, 26 Dec 2025 16:26:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_sensational/post/94459a85-c765-4c68-9c1f-407b6d43be98/image.webp" alt=""></p>
<p>오늘은 프로젝트에 NPC 상점 시스템을 구현하고, <code>NpcWorldSubsystem</code> / <code>NpcShopComponent</code> / <code>PlayerController</code> / <code>Shop UI 위젯</code> 사이의 흐름을 점검하는 작업을 진행하였습니다. </p>
<hr>
<h2 id="상점-ui-생성-전체-플로우">상점 UI 생성 전체 플로우</h2>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/b91a9b3b-b47b-4962-aca5-9d561293766e/image.png" alt=""></p>
<hr>
<h2 id="오늘-작업한-내용-정리">오늘 작업한 내용 정리</h2>
<ul>
<li><strong>데이터 구조 정리</strong><ul>
<li><code>FCMShopItemContent</code><ul>
<li><code>FName ItemID</code> : 아이템 식별용 ID</li>
<li><code>int32 BuyPrice</code> : 구매 가격</li>
<li><code>int32 SellPrice</code> : 판매 가격</li>
<li><code>int32 Quantity</code> : 수량 정보</li>
<li><code>FText ItemName</code> : 표시용 이름</li>
<li><code>UTexture2D* ItemIcon</code> : 아이콘 이미지</li>
</ul>
</li>
<li><code>UDataTable</code> 기반으로 상점 아이템 목록을 관리하도록 설계</li>
<li><code>ACMNpcBase</code> 에 <code>TObjectPtr&lt;UDataTable&gt; ShopItemDataTable</code>, <code>FName NpcId</code> 를 추가하여 NPC 단위로 상점 구성을 다르게 가져갈 수 있도록 준비</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/b265611c-e526-441c-9e23-14cc5122dbb4/image.png" alt=""></p>
<ul>
<li><p><strong>NPC 캐싱 구조 설계 (<code>UCMNpcWorldSubsystem</code>)</strong></p>
<ul>
<li><code>FCMNpcCacheEntry</code><ul>
<li><code>FName NpcId</code></li>
<li><code>TWeakObjectPtr&lt;ACMNpcBase&gt; NpcActor</code></li>
</ul>
</li>
<li><code>TMap&lt;FName, FCMNpcCacheEntry&gt; NpcCacheMap</code> 에 NPC를 ID 기반으로 캐싱</li>
<li><code>RegisterNpc(const FName&amp; NpcId, ACMNpcBase* NpcActor)</code><ul>
<li><code>NpcCacheMap.FindOrAdd(NpcId)</code> 를 이용해 캐시 엔트리 생성/조회 후 값 설정</li>
</ul>
</li>
<li><code>UnregisterNpc(const FName&amp; NpcId)</code><ul>
<li>NPC가 사라질 때 캐시에서 정리</li>
</ul>
</li>
<li><code>GetNpcById(const FName&amp; NpcId) const</code><ul>
<li>캐시에서 ID로 액터를 조회</li>
</ul>
</li>
<li><code>GetAllNpcEntries(TArray&lt;FCMNpcCacheEntry&gt;&amp; OutEntries) const</code><ul>
<li>디버깅 및 툴 제작용 전체 조회 함수 제공</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>NPC에서 Subsystem 등록 흐름</strong></p>
<ul>
<li><code>ACMNpcBase</code><ul>
<li><code>FName NpcId</code> 를 에디터에서 설정 가능하도록 노출</li>
<li>NPC가 BeginPlay 시점에 <code>UCMNpcWorldSubsystem</code> 을 가져와 <code>RegisterNpc(NpcId, this)</code> 를 호출하도록 구현</li>
<li>EndPlay 시점에는 <code>UnregisterNpc(NpcId)</code> 로 정리</li>
</ul>
</li>
<li>이를 통해 서버(호스트)에서 모든 NPC를 ID로 빠르게 찾을 수 있는 구조 확보</li>
</ul>
</li>
<li><p><strong>NPC 상점 컴포넌트 (<code>UCMNpcShopComponent</code>)</strong></p>
<ul>
<li><code>UCMNpcComponentBase</code> 를 상속</li>
<li><code>TArray&lt;FCMShopItemContent&gt; ShopItemContents</code> 를 내부에 보관</li>
<li>인터페이스<ul>
<li><code>virtual void PerformAction() override;</code></li>
<li><code>void SetShopItemContents(const TArray&lt;FCMShopItemContent&gt;&amp; InItems);</code></li>
<li><code>void GetShopItemContents(TArray&lt;FCMShopItemContent&gt;&amp; OutItems) const;</code></li>
</ul>
</li>
<li><code>PerformAction()</code> 에서의 역할<ul>
<li>NPC 오너(<code>ACMNpcBase</code>) 로부터 <code>NpcId</code> 를 가져옴</li>
<li>로컬 플레이어 컨트롤러(<code>ACMPlayerController</code>)를 찾아 <code>RequestOpenShopUI(NpcId)</code> 호출</li>
<li>상점 액션이 트리거되면 컨트롤러를 통해 상점 UI 요청이 이어지도록 연결</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>PlayerController의 상점 UI 흐름 (<code>ACMPlayerController</code>)</strong></p>
<ul>
<li>프로퍼티<ul>
<li><code>TSubclassOf&lt;UCMShopWidget&gt; ShopWidgetClass;</code> : 에디터에서 상점 메인 위젯 클래스 지정</li>
<li><code>UCMShopWidget* ShopWidgetInstance;</code> : 런타임 인스턴스 보관</li>
</ul>
</li>
<li>상점 UI 요청 진입점<ul>
<li><code>UFUNCTION(BlueprintCallable, Category = &quot;Shop|UI&quot;) void RequestOpenShopUI(const FName&amp; NpcId);</code></li>
<li>로컬 컨트롤러인지 확인 후 <code>Server_RequestShopData(NpcId)</code> 호출</li>
</ul>
</li>
<li>서버 RPC<ul>
<li><code>UFUNCTION(Server, Reliable, WithValidation) void Server_RequestShopData(const FName&amp; NpcId);</code></li>
<li>구현부에서<ul>
<li><code>UCMNpcWorldSubsystem</code> 에서 <code>GetNpcById(NpcId)</code> 로 NPC 조회</li>
<li>NPC의 <code>UCMNpcShopComponent</code> 를 찾아 <code>GetShopItemContents()</code> 로 상점 아이템 배열 획득</li>
<li><strong>Host(리슨 서버)</strong> 인 경우: <code>CreateShopWidget(ShopItems)</code> 를 바로 호출하여 서버/클라 겸용 컨트롤러에서 UI 생성</li>
<li><strong>순수 클라이언트</strong> 인 경우: <code>Client_ReceiveShopDataAndOpen(ShopItems)</code> 클라 RPC 호출</li>
</ul>
</li>
</ul>
</li>
<li>클라 RPC<ul>
<li><code>UFUNCTION(Client, Reliable) void Client_ReceiveShopDataAndOpen(const TArray&lt;FCMShopItemContent&gt;&amp; ShopItems);</code></li>
<li>구현부에서 <code>CreateShopWidget(ShopItems);</code> 호출</li>
</ul>
</li>
<li>실제 UI 생성 함수<ul>
<li><code>void CreateShopWidget(const TArray&lt;FCMShopItemContent&gt;&amp; InShopItems);</code></li>
<li><code>IsLocalController()</code> 확인 후<ul>
<li>이미 인스턴스가 있으면 <code>SetShopItems()</code> / <code>BuildShopList()</code> 만 호출하여 갱신</li>
<li>없으면 <code>UIManagerComponent-&gt;PushWidget(ShopWidgetClass)</code> 로 위젯을 스택에 올린 뒤, 데이터 주입 및 리스트 빌드</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Shop 위젯 구현 (<code>UCMShopWidget</code>)</strong></p>
<ul>
<li>구성 요소<ul>
<li><code>UVerticalBox* ShopItemListBox;</code> (BindWidget)</li>
<li><code>TSubclassOf&lt;UCMShopContentElementWidget&gt; ShopItemWidgetClass;</code></li>
<li><code>TArray&lt;FCMShopItemContent&gt; ShopItems;</code></li>
</ul>
</li>
<li>인터페이스<ul>
<li><code>void SetShopItems(const TArray&lt;FCMShopItemContent&gt;&amp; InItems);</code></li>
<li><code>void BuildShopList();</code></li>
</ul>
</li>
<li><code>BuildShopList()</code> 동작 개요<ul>
<li>기존 자식 위젯 제거</li>
<li><code>ShopItems</code>를 순회하면서 <code>ShopItemWidgetClass</code> 로 <code>UCMShopContentElementWidget</code> 생성</li>
<li>각 엘리먼트에<ul>
<li>아이템 이미지, 이름, 구매가, 판매가, 수량 등의 데이터를 바인딩</li>
<li>구매 버튼 / 판매 버튼 클릭 델리게이트를 바인딩하여 상위 위젯 (<code>UCMShopWidget</code>) 의 <code>HandleElementBuyRequested</code>, <code>HandleElementSellRequested</code> 와 연결</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>개별 상점 아이템 위젯 (<code>UCMShopContentElementWidget</code>)</strong></p>
<ul>
<li>역할<ul>
<li>한 개의 <code>FCMShopItemContent</code> 를 시각화</li>
<li>이미지, 이름, 가격(구매/판매), 수량 등을 표시</li>
<li>&quot;구매&quot; 버튼 / &quot;판매&quot; 버튼 UI 제공</li>
</ul>
</li>
<li>이벤트<ul>
<li>구매 버튼 클릭 → 상위 위젯으로 &quot;이 아이템을 구매하고 싶다&quot; 이벤트 전파</li>
<li>판매 버튼 클릭 → 상위 위젯으로 &quot;이 아이템을 판매하고 싶다&quot; 이벤트 전파</li>
</ul>
</li>
<li>상위 위젯에서 실제 로직(인벤토리 차감, 골드 변경, 서버 검증 등)을 처리할 수 있도록 설계</li>
</ul>
</li>
<li><p><strong>Host(리슨 서버) / 클라이언트 동작 차이 정리</strong></p>
<ul>
<li>공통<ul>
<li><code>UCMNpcShopComponent::PerformAction()</code> → <code>ACMPlayerController::RequestOpenShopUI(NpcId)</code> 호출</li>
</ul>
</li>
<li>Host (ListenServer)<ul>
<li><code>RequestOpenShopUI</code> → <code>Server_RequestShopData</code> 가 같은 프로세스의 서버 컨텍스트에서 실행</li>
<li>서버 내부에서 <code>NpcWorldSubsystem</code> 으로 상점 데이터 조회 후 <code>CreateShopWidget</code> 을 직접 호출</li>
<li>별도의 클라 RPC 없이도 Host 화면에 상점 UI 표시</li>
</ul>
</li>
<li>순수 클라이언트<ul>
<li><code>RequestOpenShopUI</code> → Server RPC 로 서버에 상점 데이터 요청</li>
<li>서버에서 상점 데이터 구성 후 <code>Client_ReceiveShopDataAndOpen</code> 으로 돌려줌</li>
<li>클라이언트에서 <code>CreateShopWidget</code> 을 호출해 UIManager로 Push</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>디버깅용 로그 추가</strong></p>
<ul>
<li><code>CreateShopWidget</code><ul>
<li>인자로 전달된 <code>ShopItems.Num()</code> 과 첫 번째 아이템의 <code>ItemID</code>, <code>ItemName</code>, <code>BuyPrice</code>, <code>SellPrice</code>, <code>Quantity</code> 를 로그로 출력</li>
<li>서버에서 받은 상점 데이터가 실제로 클라이언트까지 전달되었는지 확인하는 데 사용</li>
</ul>
</li>
<li><code>Server_RequestShopData</code><ul>
<li><code>NpcId</code> 와 NPC/ShopComponent 유효성 체크 로그 출력</li>
</ul>
</li>
<li><code>UCMNpcShopComponent::PerformAction</code><ul>
<li>PerformAction 이 실제로 호출되는지, Owner NPC 및 <code>NpcId</code> 가 유효한지 확인하는 로그 출력</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>오늘은 NPC 상점 시스템의 전체 흐름을 정리하고, 특히 <code>NpcWorldSubsystem</code> 기반의 NPC 캐싱과 <code>UCMNpcShopComponent</code> → <code>ACMPlayerController</code> → <code>UCMShopWidget</code> 으로 이어지는 상점 UI 생성 파이프라인을 점검하였습니다. Host(리슨 서버) 환경과 순수 클라이언트 환경에서의 동작 차이를 고려하여 Server RPC 및 Client RPC 분기를 설계한 덕분에, 네트워크 환경에 따라 상점 UI가 일관되게 동작하도록 만들 수 있었습니다. 앞으로는 이 구조 위에 실제 구매/판매 서버 검증 로직과 인벤토리, 골드 연동 로직을 추가하여 완성도 높은 상점 시스템으로 발전시켜 나가고자 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] 상점 UI 설계]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-%EC%83%81%EC%A0%90-UI-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-%EC%83%81%EC%A0%90-UI-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Tue, 23 Dec 2025 12:01:22 GMT</pubDate>
            <description><![CDATA[<p>오늘은 언리얼 엔진 UMG를 사용해 상점(Shop) UI를 설계·구현한 과정을 정리해 보았습니다. 특히 <strong>아이템별 개별 위젯 구성</strong>, <strong>구매/판매 가격 분리</strong>, <strong>아이템 ID 관리</strong>, <strong>Vertical List 기반 아이템 나열</strong>에 초점을 두고 작업을 진행하였습니다. 본 문서는 추후 상점 기능을 확장하거나 다른 인벤토리/상점 UI를 설계할 때 참고 자료로 활용하기 위해 작성하였습니다.</p>
<hr>
<h2 id="상점-요소-위젯ucmshopcontentelementwidget-설계">상점 요소 위젯(UCMShopContentElementWidget) 설계</h2>
<ul>
<li>목적<ul>
<li>상점 리스트에서 <strong>한 개의 아이템</strong>을 표현하는 전용 위젯</li>
<li>UI 관점에서 재사용 가능한 단위 컴포넌트로 설계</li>
</ul>
</li>
</ul>
<ul>
<li>표시 요소<ul>
<li>아이템 이미지: <code>UImage* ItemImage</code></li>
<li>아이템 이름: <code>UTextBlock* ItemNameText</code></li>
<li>구매 가격: <code>UTextBlock* BuyPriceText</code></li>
<li>판매 가격: <code>UTextBlock* SellPriceText</code></li>
</ul>
</li>
</ul>
<ul>
<li>동작 요소 (버튼)<ul>
<li>구매 버튼: <code>UButton* BuyButton</code></li>
<li>판매 버튼: <code>UButton* SellButton</code></li>
</ul>
</li>
</ul>
<ul>
<li>데이터 필드<ul>
<li>아이템 아이콘: <code>TObjectPtr&lt;UTexture2D&gt; ItemIcon</code></li>
<li>아이템 이름: <code>FText ItemName</code></li>
<li>구매 가격: <code>int32 BuyPrice</code></li>
<li>판매 가격: <code>int32 SellPrice</code></li>
<li>아이템 식별자: <code>int32 ItemId</code> 또는 <code>FName ItemId</code> (프로젝트 상황에 맞게 선택)</li>
</ul>
</li>
</ul>
<ul>
<li>핵심 설정 함수<ul>
<li><code>SetItemDisplayData(UTexture2D* InIcon, const FText&amp; InName, int32 InBuyPrice, int32 InSellPrice, int32 InItemId)</code><ul>
<li>아이콘, 이름, 구매/판매 가격, 아이템 ID를 한 번에 세팅</li>
<li>내부 Transient 필드에 값 저장 후, 바인딩된 위젯에 텍스트/이미지 반영</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>버튼 활성 제어<ul>
<li><code>SetButtonsEnabled(bool bEnableBuy, bool bEnableSell)</code><ul>
<li>상황에 따라 구매만 가능 / 판매만 가능 / 둘 다 비활성 등을 제어</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>버튼 클릭 이벤트 처리 흐름<ul>
<li><code>NativeOnInitialized()</code>에서 버튼 클릭 바인딩<ul>
<li><code>BuyButton-&gt;OnClicked.AddDynamic(this, &amp;UCMShopContentElementWidget::HandleBuyClicked);</code></li>
<li><code>SellButton-&gt;OnClicked.AddDynamic(this, &amp;UCMShopContentElementWidget::HandleSellClicked);</code></li>
</ul>
</li>
<li>내부 핸들러에서 델리게이트 브로드캐스트<ul>
<li><code>OnBuyRequested.Broadcast(this);</code></li>
<li><code>OnSellRequested.Broadcast(this);</code></li>
</ul>
</li>
<li>외부(상점 메인 위젯)가 이 델리게이트에 바인딩해 실제 구매/판매 로직을 처리</li>
</ul>
</li>
</ul>
<ul>
<li>외부에서 참조 가능한 Getter<ul>
<li><code>GetBuyPrice()</code> / <code>GetSellPrice()</code></li>
<li><code>GetItemName()</code> / <code>GetItemIcon()</code></li>
<li><code>GetItemId()</code></li>
<li>상위 위젯이 ElementWidget을 통해 아이템 정보를 재조회할 수 있도록 설계</li>
</ul>
</li>
</ul>
<hr>
<h2 id="상점-데이터-구조fcmshopitem-설계">상점 데이터 구조(FCMShopItem) 설계</h2>
<ul>
<li>목적<ul>
<li>상점에 노출할 아이템 정보를 모아 관리하기 위한 구조체</li>
<li>상점 위젯이 <code>TArray&lt;FCMShopItem&gt;</code>를 기준으로 리스트를 생성하도록 설계</li>
</ul>
</li>
</ul>
<ul>
<li>필드 구성<ul>
<li><code>ItemID</code>: 아이템 고유 식별자 (int32)</li>
<li><code>BuyPrice</code>: 구매 가격</li>
<li><code>SellPrice</code>: 판매 가격</li>
<li><code>Quantity</code>: 수량 (스택 수, 재고 등 다양한 용도로 확장 가능)</li>
<li><code>ItemName</code>: UI 표시용 이름</li>
<li><code>ItemIcon</code>: UI 표시용 아이콘 텍스처</li>
</ul>
</li>
</ul>
<ul>
<li>설계 포인트<ul>
<li>Blueprint에서도 손쉽게 세팅할 수 있도록 <code>BlueprintType</code>, <code>EditAnywhere</code>, <code>BlueprintReadWrite</code>로 설정</li>
<li>실제 게임 데이터(데이터 테이블, 데이터 애셋 등)와 연결하거나, 임시 테스트 데이터로도 사용 가능하게 유연하게 설계</li>
</ul>
</li>
</ul>
<hr>
<h2 id="상점-메인-위젯ucmshopwidget-설계">상점 메인 위젯(UCMShopWidget) 설계</h2>
<ul>
<li>역할<ul>
<li>전체 상점 화면을 담당하는 메인 컨테이너 위젯</li>
<li><code>VerticalBox</code> 안에 여러 <code>UCMShopContentElementWidget</code>을 동적으로 생성하여 리스트를 구성</li>
</ul>
</li>
</ul>
<ul>
<li>주요 멤버<ul>
<li><code>ShopItemListBox</code><ul>
<li>타입: <code>UVerticalBox*</code></li>
<li>역할: 상점 아이템들을 세로로 나열하는 컨테이너</li>
<li>UMG 디자이너에서 <code>BindWidget</code> 이름과 동일하게 배치</li>
</ul>
</li>
<li><code>ShopItemWidgetClass</code><ul>
<li>타입: <code>TSubclassOf&lt;UCMShopContentElementWidget&gt;</code></li>
<li>역할: 각 아이템 엘리먼트로 생성할 위젯 클래스 (BP 기반으로 지정)</li>
</ul>
</li>
<li><code>ShopItems</code><ul>
<li>타입: <code>TArray&lt;FCMShopItem&gt;</code></li>
<li>역할: 실제 상점에 표시할 아이템 데이터 목록</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>초기화 흐름<ul>
<li>생성자: <code>UCMShopWidget(const FObjectInitializer&amp; ObjectInitializer)</code></li>
<li><code>NativeOnInitialized()</code>에서 초기 리스트 빌드 호출<ul>
<li><code>BuildShopList();</code></li>
<li>디자이너에서 <code>ShopItems</code>를 미리 세팅해 둔 경우 바로 UI에 리스트가 생성되도록 구성</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>리스트 빌드 로직 (<code>BuildShopList</code>)<ul>
<li><code>ShopItemListBox</code> 또는 <code>ShopItemWidgetClass</code>가 유효하지 않으면 조기 리턴</li>
<li>기존 자식 위젯 제거: <code>ShopItemListBox-&gt;ClearChildren();</code></li>
<li><code>ShopItems</code> 배열을 순회하며 다음 작업 수행<ul>
<li><code>CreateWidget&lt;UCMShopContentElementWidget&gt;(this, ShopItemWidgetClass)</code>로 엘리먼트 위젯 생성</li>
<li><code>SetItemDisplayData</code>로 이미지/이름/구매·판매 가격/ID 설정</li>
<li><code>OnBuyRequested</code>, <code>OnSellRequested</code> 델리게이트를 메인 위젯의 핸들러에 바인딩</li>
<li><code>ShopItemListBox-&gt;AddChild(ElementWidget)</code>으로 VerticalBox에 추가</li>
<li><code>UVerticalBoxSlot</code>로 캐스팅해 정렬, 패딩 등 레이아웃 세부 설정</li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li>상점 데이터 갱신 (<code>SetShopItems</code>)<ul>
<li>외부에서 <code>TArray&lt;FCMShopItem&gt;</code>을 넘겨받아 <code>ShopItems</code>를 교체</li>
<li>교체 직후 <code>BuildShopList()</code> 재호출로 UI를 바로 갱신</li>
<li>코드/블루프린트 어디서든 상점 데이터만 갈아끼우면 UI 전체가 새로 그려지는 구조</li>
</ul>
</li>
</ul>
<ul>
<li>버튼 클릭 이벤트 최종 처리<ul>
<li><code>HandleElementBuyRequested(UCMShopContentElementWidget* ElementWidget)</code><ul>
<li><code>ElementWidget-&gt;GetItemId()</code>와 <code>GetBuyPrice()</code>로 어떤 아이템인지, 얼마인지 확인</li>
<li>실제 구매 로직 (골드 차감, 인벤토리 추가 등)과 연결 예정</li>
</ul>
</li>
<li><code>HandleElementSellRequested(UCMShopContentElementWidget* ElementWidget)</code><ul>
<li><code>ElementWidget-&gt;GetItemId()</code>와 <code>GetSellPrice()</code>를 사용</li>
<li>실제 판매 로직 (인벤토리에서 제거, 골드 지급 등)과 연결 예정</li>
</ul>
</li>
<li>상점 위젯은 <strong>UI 이벤트를 게임 로직 레이어로 전달하는 허브</strong> 역할을 담당</li>
</ul>
</li>
</ul>
<hr>
<h2 id="umg-에디터에서의-연결-작업-정리">UMG 에디터에서의 연결 작업 정리</h2>
<ul>
<li>상점 메인 위젯 BP (<code>UCMShopWidget</code> 기반)<ul>
<li>위젯 트리에서 <code>VerticalBox</code> 추가 후 이름을 <code>ShopItemListBox</code>로 지정</li>
<li>클래스 디폴트에서 <code>ShopItemWidgetClass</code>에 <code>UCMShopContentElementWidget</code> 기반 BP를 할당</li>
<li>테스트용으로 <code>ShopItems</code> 배열에 몇 개의 아이템 데이터 직접 입력해 즉시 미리보기 가능하게 설정</li>
</ul>
</li>
</ul>
<ul>
<li>상점 요소 위젯 BP (<code>UCMShopContentElementWidget</code> 기반)<ul>
<li>이미지/텍스트/버튼 위젯 배치 후 이름 매칭<ul>
<li><code>ItemImage</code></li>
<li><code>ItemNameText</code></li>
<li><code>BuyPriceText</code></li>
<li><code>SellPriceText</code></li>
<li><code>BuyButton</code></li>
<li><code>SellButton</code></li>
</ul>
</li>
<li>C++의 <code>BindWidgetOptional</code> 속성과 이름이 일치하도록 관리해 자동 바인딩 확인</li>
</ul>
</li>
</ul>
<hr>
<p>오늘은 언리얼 엔진 UMG를 사용해 상점 UI를 설계하고, 아이템 단위 위젯과 상점 메인 위젯 사이의 책임을 분리하는 구조를 구현해 보았습니다. 아이템 이미지, 이름, 구매/판매 가격, 그리고 식별용 ID까지 한 번에 관리할 수 있도록 설계함으로써 추후 실제 게임 데이터 및 인벤토리 시스템과의 연동이 훨씬 수월해질 것으로 기대하고 있습니다.</p>
<p>향후에는 오늘 설계한 UI 구조를 기반으로 실제 골드, 인벤토리, 툴팁, 필터링/정렬 기능 등을 추가하여 보다 완성도 높은 상점 시스템으로 확장해 나갈 계획입니다. 오늘의 정리가 이후 작업에 도움이 되기를 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] NPC Dialogue & Action 시스템 1차 구현 완료]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-NPC-Dialogue-Action-%EC%8B%9C%EC%8A%A4%ED%85%9C-1%EC%B0%A8-%EA%B5%AC%ED%98%84-%EC%99%84%EB%A3%8C</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-NPC-Dialogue-Action-%EC%8B%9C%EC%8A%A4%ED%85%9C-1%EC%B0%A8-%EA%B5%AC%ED%98%84-%EC%99%84%EB%A3%8C</guid>
            <pubDate>Fri, 12 Dec 2025 11:42:19 GMT</pubDate>
            <description><![CDATA[<p>이번 작업에서는 CrimsonMoon 프로젝트에 NPC 대화(Npc Dialogue) 시스템을 1차적으로 구현하고, 위젯/액터/게임 인스턴스/로컬 이벤트 매니저 간의 구조를 정리하여 구현하였습니다. 특히, C++과 UMG를 섞어 쓰는 환경에서 <strong>델리게이트 기반 이벤트 흐름</strong>과 <strong>위젯/액터 간 의존성 최소화</strong>를 목표로 진행했습니다.</p>
<hr>
<h2 id="전체-구조-개요">전체 구조 개요</h2>
<ul>
<li>목표<ul>
<li>NPC와 상호작용 시 대화 UI 표시</li>
<li>Next 버튼으로 다음 노드 진행</li>
<li>Action 노드가 있을 경우 선택지 버튼 동적 생성</li>
<li>선택지 클릭 시 해당 액션 타입으로 NPC 컴포넌트 동작 수행</li>
<li>대화 종료 시 델리게이트/상태 정리</li>
</ul>
</li>
<li>핵심 구성 요소<ul>
<li><code>ACMNpcBase</code> : NPC 액터, 대화 트리 데이터/진행 로직 보유</li>
<li><code>UCMDialoagueNode</code> / <code>UCMActionNode</code> : 대화 노드 &amp; 액션 노드 트리 구조</li>
<li><code>UCMLocalEventManager</code> : GameInstanceSubsystem, 대화용 델리게이트 허브</li>
<li><code>UCMGameInstance</code> : GameInstance, 로컬 이벤트 매니저/세션 등 상위 관리</li>
<li><code>UCMNpcDialogueWidget</code> : 대화 전체 UI, 텍스트/Next 버튼/선택지 컨테이너</li>
<li><code>UCMNpcDialogueChoiceElement</code> : 개별 선택지 버튼 위젯</li>
<li><code>UUIManagerComponent</code> : PlayerController에 부착, UI 스택 관리 및 뷰포트 표시</li>
</ul>
</li>
</ul>
<hr>
<h2 id="클래스-구조-mermaid-시각화">클래스 구조 (Mermaid 시각화)</h2>
<h3 id="상위-구조-gameinstance--subsystem--npc--ui">상위 구조: GameInstance &amp; Subsystem &amp; NPC &amp; UI</h3>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/13e7c448-f690-492c-9faf-39fb9778cfc3/image.png" alt=""></p>
<h3 id="ui-구조-dialogue-widget--choice-element">UI 구조: Dialogue Widget &amp; Choice Element</h3>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/1258ba38-ac63-437a-ba8b-91aa2e091f2d/image.png" alt=""></p>
<h3 id="대화-노드-구조-노드-트리--액션-노드">대화 노드 구조: 노드 트리 &amp; 액션 노드</h3>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/a5ece104-7d1c-471b-b621-f78e53542d1d/image.png" alt=""></p>
<hr>
<h2 id="이벤트-흐름-상호작용-→-대화-→-선택지-→-종료">이벤트 흐름 (상호작용 → 대화 → 선택지 → 종료)</h2>
<p>다음 플로우 차트는 이벤트 흐름을 나타냅니다. </p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/7108c26f-f400-4b0e-86d8-a1af4246162a/image.png" alt=""></p>
<hr>
<h2 id="구현-상의-핵심-포인트">구현 상의 핵심 포인트</h2>
<ul>
<li><p>GameInstanceSubsystem 활용</p>
<ul>
<li><code>UCMLocalEventManager</code> 를 <code>UGameInstanceSubsystem</code> 으로 두어, 맵 전환과 무관하게 대화 델리게이트를 공유 가능하게 설계</li>
<li>NPC, UI, 기타 시스템이 서로 직접 참조하지 않고 <strong>이벤트 허브</strong>를 통해 느슨하게 연결</li>
</ul>
</li>
<li><p>델리게이트 기반 UI-로직 분리</p>
<ul>
<li>NPC 쪽은 순수한 대화 트리/액션 처리에 집중</li>
<li>UI 쪽은 단지 델리게이트를 통해 텍스트와 선택지 정보를 받아 그리기만 함</li>
<li>이로 인해 UI 변경(디자인 교체, 애니메이션 추가 등)이 NPC 로직에 영향을 주지 않음</li>
</ul>
</li>
<li><p>액션 노드(선택지) 구조</p>
<ul>
<li>대화 트리는 <code>UCMDialoagueNode</code> 를 기본으로 하되, 선택지가 필요한 경우 파생 클래스 <code>UCMActionNode</code> 로 표현</li>
<li>액션 노드는<ul>
<li><code>DialogueText</code> (선택지에 표시할 텍스트)</li>
<li><code>ActionType</code> (ECMNpcComponentType) 을 가지며,</li>
<li>선택 시 NPC가 어떤 컴포넌트의 어떤 액션을 수행해야 하는지 연결고리 역할을 수행</li>
</ul>
</li>
</ul>
</li>
<li><p>EndDialogue 시 델리게이트 해제 철저</p>
<ul>
<li>대화 한 번 끝난 후에도 델리게이트가 남아있으면<ul>
<li>다음 대화에서 중복 호출, 크래시, 메모리 누수의 원인이 될 수 있음</li>
</ul>
</li>
<li><code>EndDialogue()</code> 에서 NPC ↔ LocalEventManager 간의 모든 바인딩을 명시적으로 해제</li>
</ul>
</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>이번 NPC 대화 시스템 구현은 <strong>데이터 구조(트리 기반 대화 노드)</strong>, <strong>델리게이트 기반 이벤트 흐름</strong>, <strong>UI/로직 분리</strong>, <strong>GameInstanceSubsystem 활용</strong>이라는 네 가지 축을 중심으로 설계하고 구현하였습니다. 그 과정에서 UMG 바인딩, 델리게이트 시그니처, 포인터 관리 등 언리얼 특유의 함정들을 실제로 밟아 보며 정리할 수 있었고, 이를 통해 프로젝트 전반에서 재사용 가능한 패턴을 쌓을 수 있었다고 생각합니다.</p>
<p>앞으로는 이 구조 위에 퀘스트 시스템 연동, 세이브/로드 시 대화 상태 복원, 로컬라이제이션(다국어 지원) 등을 확장해 나가면서 NPC와의 상호작용 경험을 더욱 풍부하게 만들어 볼 계획입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] NPC Dialogue 시스템 구현 중간 정리]]></title>
            <link>https://velog.io/@dev_sensational/NPC-Dialogue-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84-%EC%A4%91%EA%B0%84-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@dev_sensational/NPC-Dialogue-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84-%EC%A4%91%EA%B0%84-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 11 Dec 2025 14:39:36 GMT</pubDate>
            <description><![CDATA[<h2 id="목표">목표</h2>
<ul>
<li>NPC와 상호작용 시 대화 UI 위젯을 스택 기반으로 표시/제거</li>
<li>대화 노드 트리 기반의 대사 진행, 액션 노드 기반 선택지 버튼 생성</li>
<li>GameInstanceSubsystem(UCMLocalEventManager) 를 통한 델리게이트 브로드캐스트 구조 정립</li>
</ul>
<hr>
<h2 id="대화-데이터-구조-설계-acmnpcbase">대화 데이터 구조 설계 (ACMNpcBase)</h2>
<ul>
<li><code>UCMDialoagueNode</code><ul>
<li>부모 노드: <code>ParentNode</code></li>
<li>자식 노드 배열: <code>TArray&lt;UCMDialoagueNode*&gt; Children</code></li>
<li>대사 텍스트: <code>FText DialogueText</code></li>
</ul>
</li>
<li><code>UCMActionNode : UCMDialoagueNode</code><ul>
<li>액션 타입: <code>ECMNpcComponentType ActionType</code></li>
</ul>
</li>
<li><code>ACMNpcBase</code><ul>
<li>루트 노드: <code>RootDialogueNode</code></li>
<li>전체 노드 리스트: <code>AllDialogueNodes</code></li>
<li>현재 노드: <code>CurrentNode</code></li>
<li>노드 생성 함수들:<ul>
<li><code>CreateDialogueNode(...)</code></li>
<li><code>CreateDialogueNodeWithSettings(...)</code></li>
<li><code>CreateActionNodeWithSettings(...)</code></li>
</ul>
</li>
<li>트리 탐색/디버그<ul>
<li><code>LogAllDialogueNodeTexts()</code> 로 전체 노드 로그</li>
<li><code>MoveToChildNodeByIndex(int32 ChildIndex)</code> 로 자식 인덱스 기반 이동</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="npc-상호작용-흐름-acmnpcbase">NPC 상호작용 흐름 (ACMNpcBase)</h2>
<ul>
<li><code>BeginPlay()</code><ul>
<li>NpcComponents 기반으로 <code>UCMNpcComponentBase</code> 파생 컴포넌트 생성 및 등록</li>
<li>1초 후 <code>LogAllDialogueNodeTexts()</code> 실행 (디버그용)</li>
<li>5초 후 <code>PerformInteract()</code> 자동 호출 (테스트용)</li>
</ul>
</li>
<li><code>PerformInteract()</code><ul>
<li>현재 구현: <code>StartDialogue()</code> 호출</li>
<li>후속 작업: 필요 시 여기서 UI 생성, 대사 초기화 등 추가</li>
</ul>
</li>
<li><code>HandleActionByType(ECMNpcComponentType)</code><ul>
<li>등록된 컴포넌트 맵에서 타입에 해당하는 컴포넌트 찾아 <code>PerformAction()</code> 호출</li>
</ul>
</li>
</ul>
<hr>
<h2 id="gameinstancesubsystem-ucmlocaleventmanager">GameInstanceSubsystem: UCMLocalEventManager</h2>
<ul>
<li>역할: NPC ↔ UI 간 로컬 이벤트 허브</li>
<li>델리게이트 정의<ul>
<li><code>FOnNextDialogueNodeRequested</code><ul>
<li>다음 대화 노드 요청 (예: UI Next 버튼)</li>
</ul>
</li>
<li><code>FOnPrintDialogueNodeText(const FText&amp; DialogueText)</code><ul>
<li>현재 노드 대사 텍스트 브로드캐스트</li>
</ul>
</li>
<li><code>FOnCreateChoiceButton(const FText&amp; DialogueText, ECMNpcComponentType ComponentType)</code><ul>
<li>선택지 버튼 생성 요청 (액션 노드 정보 전달)</li>
</ul>
</li>
<li><code>FOnSelectedDialogueChoice(ECMNpcComponentType ComponentType)</code><ul>
<li>플레이어가 선택지 선택 시 브로드캐스트</li>
</ul>
</li>
</ul>
</li>
<li><code>UPROPERTY(BlueprintAssignable)</code> 로 모두 블루프린트에서 바인딩 가능하도록 노출</li>
</ul>
<hr>
<h2 id="npc와-localeventmanager-연동-acmnpcbase">NPC와 LocalEventManager 연동 (ACMNpcBase)</h2>
<ul>
<li><code>StartDialogue()</code><ul>
<li>GameInstance에서 <code>UCMLocalEventManager</code> 서브시스템 획득 및 캐싱</li>
<li><code>LocalEventManager-&gt;OnNextDialogueNodeRequested.AddDynamic(this, &amp;ACMNpcBase::OnNextDialogueNodeRequested);</code></li>
</ul>
</li>
<li><code>OnNextDialogueNodeRequested()</code> 구현<ul>
<li>전제: <code>CurrentNode</code> 가 설정되어 있어야 함</li>
<li>로직:<ol>
<li><code>CurrentNode</code> 가 nullptr 이면 리턴 + 경고 로그</li>
<li><code>CurrentNode-&gt;Children.Num()</code> 이 <strong>1이 아닐 경우</strong> 이동하지 않고 로그만 출력</li>
<li>자식이 1개일 때만 <code>CurrentNode</code> 를 그 자식 노드로 변경</li>
<li>변경된 노드의 <code>DialogueText</code> 를 <code>LocalEventManager-&gt;OnPrintDialogueNodeText.Broadcast(...)</code> 로 브로드캐스트</li>
<li>새 <code>CurrentNode</code> 의 <code>Children</code> 를 순회하면서 <code>UCMActionNode</code> 인 자식을 탐색<ul>
<li>각 액션 노드에 대해 <code>OnCreateChoiceButton.Broadcast(ActionNode-&gt;DialogueText, ActionNode-&gt;ActionType)</code> 호출</li>
</ul>
</li>
</ol>
</li>
</ul>
</li>
<li>(추가 예정) <code>HandleChoiceSelected(ECMNpcComponentType)</code><ul>
<li><code>FOnSelectedDialogueChoice</code> 를 수신하여 <code>HandleActionByType</code> 와 연동 예정</li>
</ul>
</li>
</ul>
<hr>
<h2 id="npc-대화-ui-위젯-구조-ucmnpcdialoguewidget">NPC 대화 UI 위젯 구조 (UCMNpcDialogueWidget)</h2>
<ul>
<li>상속: <code>UCMWidgetBase</code></li>
<li>바인딩 프로퍼티<ul>
<li><code>FText NpcNameText</code> / <code>NpcNameTextBlock</code></li>
<li><code>FText DialogueText</code> / <code>DialogueTextBlock</code></li>
<li><code>UButton* NextButton</code></li>
<li><code>UVerticalBox* ChoicesContainer</code></li>
<li><code>TArray&lt;UCMNpcDialogueChoiceElement*&gt; ChoiceWidgets</code></li>
<li>에디터에서 설정 가능한 선택지 클래스: <code>TSubclassOf&lt;UCMNpcDialogueChoiceElement&gt; ChoiceElementClass</code></li>
</ul>
</li>
<li>주요 함수<ul>
<li><code>NativeConstruct()</code><ul>
<li><code>NextButton-&gt;OnClicked.AddDynamic(this, &amp;UCMNpcDialogueWidget::OnNextDialogue);</code></li>
<li><code>ClearChoices()</code> 로 기존 선택지 초기화</li>
</ul>
</li>
<li><code>OnNextDialogue()</code><ul>
<li>내부에서 <strong>LocalEventManager의 OnNextDialogueNodeRequested 를 브로드캐스트</strong> 하도록 구현 예정 (현재는 TODO 상태로 비워둠)</li>
</ul>
</li>
<li><code>ClearChoices()</code><ul>
<li><code>ChoicesContainer-&gt;ClearChildren();</code></li>
<li><code>ChoiceWidgets.Empty();</code></li>
</ul>
</li>
<li><code>AddChoice(const FText&amp; InChoiceText, ECMNpcComponentType InComponentType)</code><ul>
<li>사용할 클래스 결정: <code>ChoiceElementClass</code> 가 있으면 그 BP, 없으면 <code>UCMNpcDialogueChoiceElement::StaticClass()</code></li>
<li><code>CreateWidget</code> 로 인스턴스 생성 후 텍스트 세팅, <code>ChoicesContainer</code> 에 AddChild, <code>ChoiceWidgets</code> 에 보관</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="선택지-ui-엘리먼트-ucmnpcdialoguechoiceelement">선택지 UI 엘리먼트 (UCMNpcDialogueChoiceElement)</h2>
<ul>
<li>상속: <code>UCMWidgetBase</code></li>
<li>프로퍼티<ul>
<li><code>FText ChoiceText</code></li>
<li><code>UButton* ChoiceButton</code></li>
<li><code>UTextBlock* ChoiceTextBlock</code></li>
</ul>
</li>
<li>델리게이트<ul>
<li><code>FOnChoiceClicked</code> (BlueprintAssignable)</li>
</ul>
</li>
<li>동작<ul>
<li><code>NativeConstruct()</code> 에서 <code>ChoiceButton-&gt;OnClicked</code> 에 내부 핸들러 바인딩</li>
<li><code>HandleChoiceButtonClicked()</code> → <code>OnChoiceClicked.Broadcast()</code> 호출</li>
<li><code>SetChoiceText(const FText&amp; InText)</code> 로 내부 텍스트와 TextBlock 동기화</li>
</ul>
</li>
</ul>
<hr>
<h2 id="델리게이트-연결-요약">델리게이트 연결 요약</h2>
<ul>
<li><code>UCMLocalEventManager</code><ul>
<li><code>OnNextDialogueNodeRequested</code><ul>
<li>(UI → NPC) Next 버튼으로 다음 노드 요청</li>
</ul>
</li>
<li><code>OnPrintDialogueNodeText</code><ul>
<li>(NPC → UI) 현재 대사 텍스트 전달</li>
</ul>
</li>
<li><code>OnCreateChoiceButton</code><ul>
<li>(NPC → UI) 선택지 버튼 정보 전달</li>
</ul>
</li>
<li><code>OnSelectedDialogueChoice</code><ul>
<li>(UI → NPC) 플레이어가 선택한 액션 타입 전달</li>
</ul>
</li>
</ul>
</li>
<li>현재 상태<ul>
<li>NPC 쪽: <code>StartDialogue()</code> 에서 <code>OnNextDialogueNodeRequested</code> 에 바인딩, <code>OnNextDialogueNodeRequested()</code> 구현 완료</li>
<li>UI 쪽: <code>NextButton</code> → <code>OnNextDialogue()</code> → LocalEventManager 브로드캐스트 부분은 TODO</li>
<li>선택지 클릭 → <code>OnSelectedDialogueChoice</code> 브로드캐스트, NPC의 <code>HandleChoiceSelected</code> 에서 처리하는 구조는 설계만 완료</li>
</ul>
</li>
</ul>
<hr>
<h2 id="요약">요약</h2>
<ul>
<li>UE5에서 <strong>GameInstanceSubsystem(UCMLocalEventManager)</strong> 를 사용해 NPC와 UI 사이의 대화 이벤트를 느슨하게 연결하는 패턴을 정리했다.</li>
<li>NPC 하나가 대화 트리(UCMDialoagueNode/UCMActionNode)를 소유하고, LocalEventManager 델리게이트를 통해 <strong>다음 노드 이동, 대사 텍스트 브로드캐스트, 선택지 생성</strong>까지 책임지도록 설계했다.</li>
<li>UI는 PlayerController 가 가진 <code>UUIManagerComponent</code> 의 스택을 통해 관리하며, <strong>대화 위젯을 푸시/팝</strong>하는 방식으로 게임 입력 모드와도 자연스럽게 연동했다.</li>
<li>아직 남은 TODO는 <strong>Next 버튼에서 OnNextDialogueNodeRequested 브로드캐스트</strong>, <strong>선택지 클릭 → OnSelectedDialogueChoice 브로드캐스트 → NPC HandleActionByType 연동</strong> 이며, 이 부분을 다음 단계에서 마무리할 예정.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] Tree 구조의 Dialogue 시스템 구현]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-Tree-%EA%B5%AC%EC%A1%B0%EC%9D%98-Dialogue-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-Tree-%EA%B5%AC%EC%A1%B0%EC%9D%98-Dialogue-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 09 Dec 2025 11:35:04 GMT</pubDate>
            <description><![CDATA[<p>이번 기능에서는 NPC가 단순히 한 줄 대사만 출력하는 수준을 넘어서, <strong>트리 형태로 분기되는 대화 시스템</strong>을 직접 설계하고 구현해 보았습니다. 언리얼 엔진의 <code>UObject</code> / <code>UActorComponent</code> / <code>AActor</code> 구조를 활용해, 블루프린트에서도 직관적으로 편집 가능한 형태를 목표로 했습니다. 특히, &quot;대화를 따라가다 특정 노드에서 액션(상점 열기 등)을 실행&quot;하는 흐름까지 테스트 코드로 구축해 본 것이 핵심입니다.</p>
<hr>
<h2 id="전체-구조-개요">전체 구조 개요</h2>
<ul>
<li><p>핵심 타입</p>
<ul>
<li><code>ECMNpcComponentType</code> : NPC 컴포넌트의 종류를 표현하는 enum</li>
<li><code>UCMDialoagueNode</code> : 기본 대화 노드(텍스트, 부모/자식 참조 포함)</li>
<li><code>UCMActionNode</code> : 기본 노드를 상속받아, 추가로 액션 타입을 가지는 노드</li>
<li><code>ACMNpcBase</code> : NPC 액터, 대화 트리 전체를 소유/관리</li>
<li><code>UCMNpcComponentBase</code> : NPC용 공통 컴포넌트 베이스</li>
<li><code>UCMNpcShopComponent</code> : 예시용 상점 컴포넌트(PerformAction 시 화면 메시지 출력)</li>
</ul>
</li>
<li><p>설계 포인트</p>
<ul>
<li><strong>대화 노드는 UObject(UCLASS)</strong> 로 구현 → GC 및 블루프린트 호환성 확보</li>
<li><strong>트리 구조</strong>는 <code>ParentNode</code> + <code>Children</code> 배열로 표현</li>
<li>NPC 액터(<code>ACMNpcBase</code>)가<ul>
<li><code>RootDialogueNode</code> (루트 노드)</li>
<li><code>AllDialogueNodes</code> (생성된 모든 노드)
를 UPROPERTY로 보유 → GC에 안전하고, 순회/디버그에 용이</li>
</ul>
</li>
<li>액션 노드는 별도 타입(<code>UCMActionNode</code>)으로 구분<ul>
<li><code>ActionType: ECMNpcComponentType</code></li>
<li>트리 순회 중 액션 노드를 만나면 <code>HandleActionByType</code> 호출</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="ecmnpccomponenttype-블루프린트에서-쓰는-컴포넌트-타입-enum">ECMNpcComponentType: 블루프린트에서 쓰는 컴포넌트 타입 enum</h2>
<pre><code class="language-cpp">UENUM(BlueprintType)
enum class ECMNpcComponentType: uint8
{
    Default UMETA(DisplayName = &quot;Default&quot;),
    DialogueComponent UMETA(DisplayName = &quot;Dialogue Component&quot;),
    QuestComponent UMETA(DisplayName = &quot;Quest Component&quot;),
    ShopComponent UMETA(DisplayName = &quot;Shop Component&quot;),
};</code></pre>
<ul>
<li>역할<ul>
<li>NPC에 붙는 컴포넌트의 종류(대화, 퀘스트, 상점 등)를 식별하기 위한 enum</li>
<li><code>ACMNpcBase</code>의 <code>RegisteredComponentMap</code> 키로 사용</li>
<li><code>UCMActionNode</code>의 <code>ActionType</code>에도 사용 → 트리 상에서 어느 컴포넌트를 실행할지 지정</li>
</ul>
</li>
</ul>
<ul>
<li>선언 방식<ul>
<li>언리얼 리플렉션 + 블루프린트 노출을 위해 <code>UENUM(BlueprintType)</code> 사용</li>
<li>기본형은 <code>int</code> (언더라이잉 타입 명시 생략)으로 유지</li>
</ul>
</li>
</ul>
<hr>
<h2 id="ucmdialoaguenode-기본-대화-노드-설계">UCMDialoagueNode: 기본 대화 노드 설계</h2>
<ul>
<li>타입<ul>
<li><code>UCLASS(BlueprintType) class UCMDialoagueNode : public UObject</code></li>
</ul>
</li>
</ul>
<pre><code class="language-cpp">UCLASS(BlueprintType)
class UCMDialoagueNode : public UObject
{
    GENERATED_BODY()

public:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=&quot;Dialogue&quot;)
    TObjectPtr&lt;UCMDialoagueNode&gt; ParentNode = nullptr;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=&quot;Dialogue&quot;)
    TArray&lt;TObjectPtr&lt;UCMDialoagueNode&gt;&gt; Children;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=&quot;Dialogue&quot;)
    FText DialogueText;
};</code></pre>
<ul>
<li><p>주요 필드</p>
<ul>
<li><code>ParentNode : UCMDialoagueNode*</code><ul>
<li>UPROPERTY(VisibleAnywhere, BlueprintReadOnly)</li>
<li>상위 노드 참조 (루트의 경우 nullptr)</li>
</ul>
</li>
<li><code>Children : TArray&lt;UCMDialoagueNode*&gt;</code><ul>
<li>UPROPERTY(EditAnywhere, BlueprintReadWrite)</li>
<li>자식 노드들(다중 분기 지원)</li>
</ul>
</li>
<li><code>DialogueText : FText</code><ul>
<li>UPROPERTY(EditAnywhere, BlueprintReadWrite)</li>
<li>실제로 화면/UI에 보여줄 대사 텍스트</li>
</ul>
</li>
</ul>
</li>
<li><p>특징</p>
<ul>
<li>UObject 기반이라 <strong>언리얼 GC 관리 대상</strong></li>
<li>블루프린트에서<ul>
<li>노드의 텍스트를 수정</li>
<li>Parent/Children를 직접 연결하여 트리 구성 가능</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="ucmactionnode-액션을-수행하는-특수-대화-노드">UCMActionNode: 액션을 수행하는 특수 대화 노드</h2>
<ul>
<li>타입<ul>
<li><code>UCLASS(BlueprintType) class UCMActionNode : public UCMDialoagueNode</code></li>
</ul>
</li>
</ul>
<pre><code class="language-cpp">UCLASS(BlueprintType)
class UCMActionNode : public UCMDialoagueNode
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=&quot;Dialogue&quot;)
    ECMNpcComponentType ActionType;
};</code></pre>
<ul>
<li><p>추가 필드</p>
<ul>
<li><code>ActionType : ECMNpcComponentType</code><ul>
<li>UPROPERTY(EditAnywhere, BlueprintReadWrite)</li>
<li>이 노드에 도달했을 때 어떤 타입의 NPC 컴포넌트를 실행할지 지정</li>
</ul>
</li>
</ul>
</li>
<li><p>동작</p>
<ul>
<li>트리 순회 중 <code>UCMActionNode</code>로 캐스팅에 성공하면<ul>
<li><code>ActionType</code> 값을 읽어</li>
<li><code>ACMNpcBase::HandleActionByType(ActionType)</code> 호출</li>
</ul>
</li>
<li>예: <code>ShopComponent</code> → 상점 열기 컴포넌트의 <code>PerformAction</code> 호출</li>
</ul>
</li>
</ul>
<hr>
<h2 id="acmnpcbase-npc-액터와-대화-트리-관리">ACMNpcBase: NPC 액터와 대화 트리 관리</h2>
<ul>
<li><p>타입</p>
<ul>
<li><code>class ACMNpcBase : public AActor, public ICMNpcHandler</code></li>
</ul>
</li>
<li><p>주요 프로퍼티</p>
<ul>
<li><code>RootDialogueNode : UCMDialoagueNode*</code><ul>
<li>대화 트리의 루트 노드</li>
</ul>
</li>
<li><code>AllDialogueNodes : TArray&lt;UCMDialoagueNode*&gt;</code><ul>
<li>생성된 모든 대화 노드 모음 (GC + 디버그용)</li>
</ul>
</li>
<li><code>CurrentNode : UCMDialoagueNode*</code><ul>
<li>런타임에 현재 플레이어가 위치한 노드(향후 사용 예정)</li>
</ul>
</li>
<li><code>NpcComponents : TArray&lt;TSubclassOf&lt;UCMNpcComponentBase&gt;&gt;</code><ul>
<li>에디터에서 지정하는 NPC용 컴포넌트 클래스 목록</li>
</ul>
</li>
<li><code>ActiveNpcComponents : TArray&lt;UCMNpcComponentBase*&gt;</code><ul>
<li>BeginPlay에서 실제 인스턴스로 생성 후 보관</li>
</ul>
</li>
<li><code>RegisteredComponentMap : TMap&lt;ECMNpcComponentType, UCMNpcComponentBase*&gt;</code><ul>
<li>각 컴포넌트 타입에 해당하는 인스턴스를 등록/맵핑</li>
</ul>
</li>
</ul>
</li>
<li><p>BeginPlay 흐름</p>
<ul>
<li><code>RegisteredComponentMap.Empty()</code> : 초기화</li>
<li><code>NpcComponents</code>를 순회하며<ul>
<li><code>NewObject&lt;UCMNpcComponentBase&gt;(this, ComponentClass)</code> 로 생성</li>
<li><code>RegisterComponent()</code> 호출 → 언리얼 라이프사이클에 등록</li>
<li><code>ActiveNpcComponents</code>에 추가</li>
</ul>
</li>
<li>생성된 각 컴포넌트는 자기 <code>BeginPlay</code>에서<ul>
<li>핸들러(<code>ACMNpcBase</code>)에 <code>RegisterComponent</code>를 호출하여 스스로 등록</li>
</ul>
</li>
<li>마지막에 테스트용:<ul>
<li><code>FTimerHandle</code>을 사용해 BeginPlay + 1초 후 <code>LogAllDialogueNodeTexts()</code> 실행</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="노드-생성-함수-설계-acmnpcbase">노드 생성 함수 설계 (ACMNpcBase)</h2>
<h3 id="createdialoguenode">CreateDialogueNode</h3>
<ul>
<li><p>시그니처</p>
<ul>
<li><code>UCMDialoagueNode* CreateDialogueNode(TSubclassOf&lt;UCMDialoagueNode&gt; NodeClass, const FText&amp; InDialogueText);</code></li>
</ul>
</li>
<li><p>역할</p>
<ul>
<li>특정 <code>NodeClass</code> (기본은 <code>UCMDialoagueNode</code>)로 새 노드를 생성하고 텍스트 설정</li>
<li><code>AllDialogueNodes</code> 배열에 추가</li>
<li>첫 번째 생성 노드는 자동으로 <code>RootDialogueNode</code>로 설정</li>
</ul>
</li>
<li><p>동작 요약</p>
<ul>
<li><code>if (!*NodeClass) NodeClass = UCMDialoagueNode::StaticClass();</code></li>
<li><code>NewObject&lt;UCMDialoagueNode&gt;(this, NodeClass)</code></li>
<li><code>NewNode-&gt;DialogueText = InDialogueText;</code></li>
<li><code>AllDialogueNodes.Add(NewNode);</code></li>
<li><code>RootDialogueNode</code>가 비어 있으면 첫 노드를 루트로 등록</li>
</ul>
</li>
</ul>
<h3 id="createdialoguenodewithsettings">CreateDialogueNodeWithSettings</h3>
<ul>
<li><p>시그니처</p>
<ul>
<li><code>UCMDialoagueNode* CreateDialogueNodeWithSettings(TSubclassOf&lt;UCMDialoagueNode&gt; NodeClass, UCMDialoagueNode* Parent, const FText&amp; InDialogueText);</code></li>
</ul>
</li>
<li><p>역할</p>
<ul>
<li><code>CreateDialogueNode</code>를 호출해 노드를 생성하고</li>
<li>부모/자식 관계를 동시에 설정하는 편의 함수</li>
</ul>
</li>
<li><p>연결 로직</p>
<ul>
<li><code>NewNode-&gt;ParentNode = Parent;</code></li>
<li><code>Parent-&gt;Children.Add(NewNode);</code></li>
</ul>
</li>
</ul>
<h3 id="createactionnodewithsettings">CreateActionNodeWithSettings</h3>
<ul>
<li><p>시그니처</p>
<ul>
<li><code>UCMDialoagueNode* CreateActionNodeWithSettings(TSubclassOf&lt;UCMActionNode&gt; NodeClass, UCMDialoagueNode* Parent, const FText&amp; InDialogueText, ECMNpcComponentType InActionType);</code></li>
</ul>
</li>
<li><p>역할</p>
<ul>
<li><code>UCMActionNode</code> 타입으로 노드를 생성</li>
<li><code>DialogueText</code> + <code>ActionType</code> + 부모/자식 관계를 한 번에 설정</li>
</ul>
</li>
<li><p>동작 요약</p>
<ul>
<li><code>if (!*NodeClass) NodeClass = UCMActionNode::StaticClass();</code></li>
<li><code>NewObject&lt;UCMActionNode&gt;(this, NodeClass)</code> 로 액션 노드 생성</li>
<li><code>NewNode-&gt;DialogueText = InDialogueText;</code></li>
<li><code>NewNode-&gt;ActionType = InActionType;</code></li>
<li><code>Parent</code>가 있으면<ul>
<li><code>NewNode-&gt;ParentNode = Parent;</code></li>
<li><code>Parent-&gt;Children.Add(NewNode);</code></li>
</ul>
</li>
</ul>
</li>
<li><p>블루프린트 사용성</p>
<ul>
<li><code>InActionType</code>가 <code>ECMNpcComponentType</code> 이라 BP 노드에서 드롭다운으로 선택 가능</li>
<li>BP에서 함수를 배치할 때, 트리 상에 어떤 액션을 둘지 직관적으로 설정 가능</li>
</ul>
</li>
</ul>
<hr>
<h2 id="블루프린트에서의-사용-패턴">블루프린트에서의 사용 패턴</h2>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/9c16ca4f-1359-425b-aafd-43c3d0d40c59/image.png" alt=""></p>
<ul>
<li><p>기본 대화 노드 생성</p>
<ul>
<li><code>CreateDialogueNode(UCMDialoagueNode, &quot;대사 텍스트&quot;)</code></li>
<li>또는 <code>CreateDialogueNodeWithSettings(UCMDialoagueNode, ParentNode, &quot;텍스트&quot;)</code></li>
</ul>
</li>
<li><p>액션 노드 생성</p>
<ul>
<li><code>CreateActionNodeWithSettings(UCMActionNode, ParentNode, &quot;텍스트&quot;, ECMNpcComponentType::ShopComponent)</code></li>
<li>분기 지점에 여러 액션 노드를 붙여서 다양한 상호작용 표현</li>
</ul>
</li>
</ul>
<ul>
<li>트리 테스트<ul>
<li>NPC 스폰 → BeginPlay 후 1초 뒤</li>
<li><code>LogAllDialogueNodeTexts()</code> 자동 호출</li>
<li>Output Log에서<ul>
<li>각 노드 텍스트</li>
<li>어떤 액션 타입이 처리되었는지 로그</li>
</ul>
</li>
<li>화면(OnScreenDebug)에서는 상점 액션 등 실제 PerformAction의 효과 확인 가능</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/16dd127c-cf1c-4c15-8efa-863eace0d4b0/image.png" alt=""></p>
<hr>
<h2 id="결론">결론</h2>
<p>이번 NPC Dialogue 시스템 구현에서는 단순한 대사 나열을 넘어서, <strong>트리 구조와 액션 노드를 결합한 대화 흐름</strong>을 구축해 보았습니다. 대화 노드를 <code>UObject</code>로 분리하고, NPC 액터가 트리 전체를 관리하도록 설계함으로써 재사용성과 확장성을 높이고자 하였습니다. 또한, 블루프린트에서 노드를 생성하고 텍스트/액션 타입을 직관적으로 지정할 수 있도록 API를 정리한 덕분에, 디자이너 입장에서의 사용성도 어느 정도 확보할 수 있었습니다.</p>
<p>앞으로는 이 구조를 기반으로 실제 게임 플레이에 필요한 UI 연동, 선택지 표시, 조건부 분기(퀘스트 진행도, 인벤토리 상태 등)에 따른 동적 트리 탐색 등을 추가해 볼 예정입니다. 이번 작업을 통해 언리얼 엔진의 UObject/Actor/Component 구조와 블루프린트 연동 방식에 대한 이해를 한층 더 깊게 할 수 있었던 의미 있는 경험이었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Memory] Image 영역?]]></title>
            <link>https://velog.io/@dev_sensational/Memory-Image-%EC%98%81%EC%97%AD</link>
            <guid>https://velog.io/@dev_sensational/Memory-Image-%EC%98%81%EC%97%AD</guid>
            <pubDate>Mon, 08 Dec 2025 11:27:45 GMT</pubDate>
            <description><![CDATA[<p>메모리 분석 유틸리티 중 하나인 VMmap에 대해 알게되어 사용하게 되었습니다. 그런데 제가 아는 것과는 다른 부분이 몇 가지 존재하여 블로그에 정리하게 되었습니다.</p>
<p>먼저 일반적으로 알려진 메모리 구조는 다음과 같습니다.</p>
<h3 id="주소-↑">주소 ↑</h3>
<table>
<thead>
<tr>
<th>영역 이름</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Kernel space</strong></td>
<td>운영체제가 사용하는 영역. 사용자 프로세스는 접근 불가.</td>
</tr>
<tr>
<td><strong>Heap</strong></td>
<td><code>malloc/new</code>로 사용하는 동적 메모리. 보통 위쪽(주소↑)으로 성장. 단편화 가능.</td>
</tr>
<tr>
<td><em>(Heap과 Stack 사이 빈 공간)</em></td>
<td>힙이 확장되거나 스택이 내려오면서 충돌 여지가 있는 공간.</td>
</tr>
<tr>
<td><strong>Stack</strong></td>
<td>함수 호출 시 스택 프레임이 아래 방향(주소↓)으로 쌓임. 지역변수/리턴주소 저장.</td>
</tr>
<tr>
<td><strong>BSS / Data</strong></td>
<td>BSS: 초기값 0 전역변수, Data: 초기화된 전역변수.</td>
</tr>
<tr>
<td><strong>Text(Code)</strong></td>
<td>실행 코드 영역. 보통 Read + Execute, Write 불가.</td>
</tr>
<tr>
<td>### 주소 ↓</td>
<td></td>
</tr>
</tbody></table>
<p>하지만 VMMap에서의 구성은 다음과 같았습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/8ffb3203-c383-4aac-abe9-ac4ab288707e/image.png" alt=""></p>
<p>여기서 알게 된 점은 Image가 우리가 알고 있던 Code가 저장되는 영역이라는 점입니다.  실제로 더미코드를 아주 많이 추가했을 때 아래와 같은 차이점을 보였습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/74cf103c-ad9e-4c31-9550-5dcb5412b8d7/image.png" alt=""></p>
<p>도움이 되는 유틸리티를 사용할 때, 사용되는 키워드의 차이점을 인지해야 효율적으로 사용할 수 있겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] Symbol]]></title>
            <link>https://velog.io/@dev_sensational/C-Symbol</link>
            <guid>https://velog.io/@dev_sensational/C-Symbol</guid>
            <pubDate>Thu, 04 Dec 2025 13:33:45 GMT</pubDate>
            <description><![CDATA[<p>링크 에러 메세지에서 주로 보게되는 심볼(Symbol)이 문득 정확히 어떻게 사용되는지, 정확히 무엇을 의미하는지가 궁금해져 정리를 하게 되었습니다. 겉핥기로 알고 있던 심볼에 대해 더 공부하고 정리해보겠습니다.</p>
<hr>
<h2 id="심볼symbol이란">심볼(Symbol)이란?</h2>
<p><strong>심볼은 컴파일러·링커가 프로그램 내부 엔티티를 식별하기 위해 생성하는 이름 정보입니다.</strong> 즉, 우리가 작성한 C++ 코드를 <strong>기계어가 이해할 수 있도록 내부적으로 사용하는 식별자</strong>입니다. </p>
<p>C++에서 심볼이 만들어지는 대상은 다음과 같습니다.</p>
<ul>
<li>함수</li>
<li>전역 변수</li>
<li>static 변수</li>
<li>클래스 멤버 함수</li>
<li>템플릿 인스턴스</li>
<li>가상 함수 테이블(vtable)</li>
</ul>
<p>심볼은 실제 바이너리 내부에 기록되며, 링커가 여러 개의 obj 파일을 연결할 때 이 정보를 사용합니다.</p>
<hr>
<h2 id="c-심볼의-실제-형태-name-mangling">C++ 심볼의 실제 형태 (Name Mangling)</h2>
<p>C++ 함수는 오버로딩, 네임스페이스, 클래스 등 구조가 복잡해서 <strong>컴파일하면 내부적으로 이름이 변형됩니다</strong>. 이를 네임 맹글링(Name Mangling) 이라고 합니다.</p>
<blockquote>
</blockquote>
<p>예를 들어:</p>
<blockquote>
</blockquote>
<pre><code class="language-cpp">int Add(int a, int b);</code></pre>
<blockquote>
</blockquote>
<p>이 함수는 컴파일 후 다음과 같은 이름으로 변환될 수 있습니다.</p>
<blockquote>
</blockquote>
<pre><code>?Add@@YAHHH@Z</code></pre><p>이런 형태의 이름이 바로 <strong>심볼(Symbol)</strong> 입니다.</p>
<hr>
<h2 id="3-심볼과-링키지linkage">3. 심볼과 링키지(Linkage)</h2>
<p>심볼에는 &quot;어디까지 보이는가?&quot; 를 나타내는 <strong>링키지(Linkage)</strong> 개념이 붙습니다.</p>
<h3 id="✔-external-linkage">✔ External Linkage</h3>
<p>다른 파일에서도 접근 가능한 심볼
(기본적인 전역 함수/전역 변수)</p>
<pre><code class="language-cpp">int gValue;
void Foo();</code></pre>
<h3 id="✔-internal-linkage">✔ Internal Linkage</h3>
<p>파일 내부에서만 보이는 심볼
<code>static</code> 키워드로 생성됩니다.</p>
<pre><code class="language-cpp">static int localGlobal = 10;</code></pre>
<h3 id="✔-no-linkage">✔ No Linkage</h3>
<p>지역 변수처럼 링커가 관여하지 않는 경우, 심볼 테이블에 올라가지 않음</p>
<hr>
<h2 id="undefined-symbol-미정의-심볼">Undefined Symbol (미정의 심볼)</h2>
<p>C++ 컴파일 시 선언만 있고 정의가 없는 심볼은 링커가 처리해야 합니다.</p>
<blockquote>
</blockquote>
<p>예:</p>
<blockquote>
</blockquote>
<pre><code class="language-cpp">extern int Value;</code></pre>
<blockquote>
</blockquote>
<p><code>Value</code>의 정의가 없다면 링크 단계에서 오류가 발생합니다.</p>
<blockquote>
</blockquote>
<pre><code>undefined reference to `Value`</code></pre><p>즉, 심볼은 <strong>선언 → 참조 → 정의</strong>가 일치해야 linking이 성공합니다.</p>
<hr>
<h2 id="symbol-table-심볼-테이블">Symbol Table (심볼 테이블)</h2>
<p>컴파일러와 링커는 심볼 정보를 다음과 같이 테이블로 관리합니다</p>
<ul>
<li>변수 이름, 타입, 메모리 위치</li>
<li>함수의 주소</li>
<li>템플릿 인스턴스 정보</li>
<li>가상 함수 테이블 정보</li>
<li>접근 범위 및 링크 여부</li>
</ul>
<p><code>.obj</code> 파일을 열면 심볼 테이블을 확인할 수 있습니다.</p>
<p>실제 심볼 테이블을 확인해보기 위해 <code>DUMPBIN</code>을 사용해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/cd661ca1-18bb-4e7f-b680-da6489f202ae/image.png" alt=""></p>
<p>VS2022의 터미널에서 실행했습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/b17f9012-e8fa-4d6c-8357-f27e074f8f14/image.png" alt=""></p>
<p>obj 파일에서 심볼 테이블을 확인할 수 있었습니다.</p>
<hr>
<h2 id="왜-중요한가">왜 중요한가?</h2>
<p>심볼은 다음 문제들의 핵심 원인/해결책이 됩니다.</p>
<ul>
<li>함수 중복 정의 오류</li>
<li>undefined reference 링크 오류</li>
<li>static 전역과 일반 전역 변수 차이</li>
<li>템플릿 인스턴스 중복 생성 문제</li>
<li>vtable 관련 링크 에러</li>
</ul>
<p>즉, <strong>C++ 빌드 과정에서 발생하는 대부분의 문제는 결국 심볼에 대한 이해로 해결됩니다.</strong></p>
<hr>
<h2 id="핵심-요약">핵심 요약</h2>
<ul>
<li>심볼은 C++ 코드의 <strong>컴파일·링크 과정에서 생성되는 내부 이름</strong></li>
<li>C++은 오버로딩 때문에 <strong>Name Mangling</strong>이 적용된다.</li>
<li>심볼에는 <strong>External / Internal / No Linkage</strong>가 있다.</li>
<li>정의되지 않은 심볼은 <strong>링크 에러</strong>를 만든다.</li>
<li>obj 파일 내부의 모든 정보는 <strong>Symbol Table</strong>에 기록되어 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] NPC 시스템 설계]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-NPC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-NPC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Wed, 03 Dec 2025 10:35:29 GMT</pubDate>
            <description><![CDATA[<p>오늘은 <code>ACMNpcBase</code>를 중심으로, 다양한 NPC 기능(대화, 퀘스트, 상점 등)을 <strong>컴포넌트 기반</strong>으로 확장할 수 있는 구조를 설계하였습니다. 이 구조를 통해 이후 요구사항 변화에 유연하게 대응할 수 있도록 확장성과 유지보수성을 고려한 아키텍처를 목표로 하였습니다.</p>
<hr>
<h2 id="설계-목표">설계 목표</h2>
<ul>
<li>컴포넌트 기반 NPC 확장 구조 설계<ul>
<li>공통된 NPC 베이스 클래스: <code>ACMNpcBase</code></li>
<li>기능 단위의 컴포넌트: <code>UCMNpcComponentBase</code> 파생 클래스들</li>
</ul>
</li>
<li>기능별 책임 분리<ul>
<li>대화(Dialogue), 퀘스트(Quest), 상점(Shop) 기능을 각각 독립된 컴포넌트로 관리</li>
</ul>
</li>
<li>타입 기반 액션 처리<ul>
<li><code>ECMNpcComponentType</code> + <code>HandleActionByType()</code> 조합으로 컴포넌트 접근</li>
</ul>
</li>
<li>향후 기능 추가에 대비한 확장성<ul>
<li>새로운 NPC 기능을 추가할 때 기존 코드 수정을 최소화</li>
</ul>
</li>
</ul>
<hr>
<h2 id="핵심-클래스-및-인터페이스-구조">핵심 클래스 및 인터페이스 구조</h2>
<ul>
<li><code>ACMNpcBase : AActor, ICMNpcHandler</code><ul>
<li>NPC의 공통 베이스 액터</li>
<li><code>ICMNpcHandler</code> 인터페이스를 구현하여 외부에서 통합된 방식으로 NPC에 요청 전달</li>
</ul>
</li>
<li><code>ICMNpcHandler</code><ul>
<li>NPC가 공통으로 가져야 할 행동 인터페이스 정의</li>
<li>예: <code>HandleActionByType(ECMNpcComponentType ComponentType)</code></li>
</ul>
</li>
<li><code>UCMNpcComponentBase</code><ul>
<li>NPC 기능용 베이스 컴포넌트 클래스</li>
<li>Dialogue / Quest / Shop 등 기능 컴포넌트의 부모 타입</li>
</ul>
</li>
<li><code>ECMNpcComponentType</code><ul>
<li>NPC 컴포넌트 타입 식별용 enum</li>
<li><code>Default</code>, <code>DialogueComponent</code>, <code>QuestComponent</code>, <code>ShopComponent</code> 등 정의</li>
</ul>
</li>
</ul>
<hr>
<h2 id="클래스-다이어그램">클래스 다이어그램</h2>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/938aac1a-a10e-416f-965c-be1f28178dbb/image.png" alt=""></p>
<hr>
<h2 id="acmnpcbase의-역할-및-책임-정리">ACMNpcBase의 역할 및 책임 정리</h2>
<ul>
<li>NPC 공통 Actor 베이스<ul>
<li>카메라 컴포넌트: <code>NpcCameraComponent</code></li>
<li>캡슐 콜라이더: <code>CapsuleComponent</code></li>
<li>스켈레탈 메시: <code>MeshComponent</code></li>
</ul>
</li>
<li>NPC 기능 컴포넌트 관리<ul>
<li><code>NpcComponents</code> 배열로 부착된 컴포넌트들을 보관</li>
<li><code>RegisteredComponentMap&lt;ECMNpcComponentType, UCMNpcComponentBase*&gt;</code>로 타입-컴포넌트 매핑</li>
</ul>
</li>
<li>컴포넌트 등록 로직<ul>
<li><code>RegisterComponent(ECMNpcComponentType ComponentType, UCMNpcComponentBase* NewComponent)</code><ul>
<li>외부/컴포넌트에서 자기 자신을 등록하는 공용 인터페이스</li>
</ul>
</li>
<li><code>PerformRegisterComponent(...)</code><ul>
<li>중복 등록 체크, 맵 갱신 등 실제 등록 로직 처리</li>
</ul>
</li>
</ul>
</li>
<li>상호작용 처리<ul>
<li><code>PerformInteract()</code><ul>
<li>플레이어가 NPC와 상호작용할 때 호출되는 Entry Point</li>
<li>적절한 <code>ECMNpcComponentType</code>를 결정 후 <code>HandleActionByType()</code> 호출 가능</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="icmnpchandler-인터페이스-활용">ICMNpcHandler 인터페이스 활용</h2>
<ul>
<li>공통 액션 처리 진입점 제공<ul>
<li><code>HandleActionByType(ECMNpcComponentType ComponentType)</code><ul>
<li>컴포넌트 타입 기반으로 적절한 NPC 컴포넌트를 찾아 액션 실행</li>
</ul>
</li>
</ul>
</li>
<li>컴포넌트 등록 책임의 일원화<ul>
<li><code>RegisterComponent(ECMNpcComponentType ComponentType, UCMNpcComponentBase* NewComponent)</code><ul>
<li>NPC 베이스에서만 타입-컴포넌트 매핑을 관리하도록 강제</li>
</ul>
</li>
</ul>
</li>
<li>장점<ul>
<li>외부에서 NPC의 내부 구조(어떤 컴포넌트가 있는지)를 몰라도, enum 타입과 핸들러 메서드만으로 행동 요청 가능</li>
<li>추후 새로운 컴포넌트 타입이 추가되어도, 인터페이스 시그니처는 유지되므로 기존 호출부 영향 최소화</li>
</ul>
</li>
</ul>
<hr>
<h2 id="컴포넌트-기반-확장성-dialogue--quest--shop">컴포넌트 기반 확장성 (Dialogue / Quest / Shop)</h2>
<ul>
<li>컴포넌트 유형 정의<ul>
<li><code>ECMNpcComponentType::DialogueComponent</code></li>
<li><code>ECMNpcComponentType::QuestComponent</code></li>
<li><code>ECMNpcComponentType::ShopComponent</code></li>
</ul>
</li>
<li>각 기능의 책임 분리<ul>
<li>Dialogue 컴포넌트<ul>
<li>NPC 대사를 관리하고, 트리 기반 대화 흐름 제어</li>
</ul>
</li>
<li>Quest 컴포넌트<ul>
<li>퀘스트 제공, 진행 상태 관리, 완료 조건 체크</li>
</ul>
</li>
<li>Shop 컴포넌트<ul>
<li>상점 인벤토리, 구매/판매 로직 관리</li>
</ul>
</li>
</ul>
</li>
<li>확장성 포인트<ul>
<li>새로운 기능(예: <code>ECMNpcComponentType::TrainingComponent</code>, <code>CraftComponent</code> 등)을 추가할 때<ul>
<li>enum에 타입만 추가</li>
<li>해당 타입을 담당하는 <code>UCMNpcComponentBase</code> 파생 컴포넌트 생성</li>
<li><code>RegisterComponent()</code>를 통해 <code>ACMNpcBase</code>에 등록</li>
</ul>
</li>
<li>기존 <code>HandleActionByType()</code> 호출 구조는 그대로 유지</li>
</ul>
</li>
<li>유지보수성 향상 요소<ul>
<li>기능 단위로 클래스가 나뉘어 있어, 버그 수정 또는 기능 변경 시 해당 컴포넌트에만 집중 가능</li>
<li>공통 인터페이스(<code>ICMNpcHandler</code>, <code>UCMNpcComponentBase</code>)를 통해 일관된 사용법 제공</li>
</ul>
</li>
</ul>
<hr>
<h2 id="8-handleactionbytype-기반-액션-라우팅">8. HandleActionByType() 기반 액션 라우팅</h2>
<ul>
<li>목적<ul>
<li>외부 코드(플레이어 상호작용, 트리거, UI 등)에서 NPC에게 특정 기능을 요청할 때, 컴포넌트를 직접 참조하지 않고 타입 기반으로 접근</li>
</ul>
</li>
<li>처리 흐름<ul>
<li>입력: <code>ECMNpcComponentType ComponentType</code></li>
<li><code>RegisteredComponentMap</code>에서 해당 타입의 컴포넌트 조회</li>
<li>컴포넌트가 존재하면 <code>UCMNpcComponentBase::HandleAction()</code> (또는 유사 메서드) 호출</li>
</ul>
</li>
<li>장점<ul>
<li>NPC 내부 구조 변경(컴포넌트 교체, 리팩토링)에도 외부 인터페이스 유지</li>
<li>다양한 상호작용(예: 대화 / 퀘스트 수락 / 상점 열기)을 <strong>타입 하나로 추상화</strong> 가능</li>
</ul>
</li>
</ul>
<hr>
<h2 id="트리형-dialogue-설계와-handleactionbytype-연계">트리형 Dialogue 설계와 HandleActionByType() 연계</h2>
<ul>
<li>트리형 Dialogue 구조 계획<ul>
<li>노드 기반 대화 구조<ul>
<li>각 노드는 대사 내용, 선택지, 다음 노드에 대한 참조를 가짐</li>
</ul>
</li>
<li>플레이어 선택에 따라 다른 노드로 분기하는 트리(또는 DAG) 형태</li>
</ul>
</li>
<li>Dialogue 컴포넌트 역할<ul>
<li>내부에 대화 트리 데이터 구조 보관</li>
<li><code>StartDialogue()</code>, <code>ProceedToNextNode(ChoiceIndex)</code>, <code>EndDialogue()</code> 등의 메서드 제공</li>
<li><code>HandleAction()</code>에서 초기 진입점(<code>StartDialogue()</code>) 호출</li>
</ul>
</li>
<li>HandleActionByType() 활용 시나리오<ul>
<li>플레이어가 NPC와 상호작용 → <code>ACMNpcBase::PerformInteract()</code> 호출</li>
<li>현재 상황(퀘스트 단계, 게임 모드 등)에 따라 아래 중 하나를 선택<ul>
<li><code>HandleActionByType(ECMNpcComponentType::DialogueComponent)</code></li>
<li><code>HandleActionByType(ECMNpcComponentType::QuestComponent)</code></li>
<li><code>HandleActionByType(ECMNpcComponentType::ShopComponent)</code></li>
</ul>
</li>
<li>Dialogue 선택 시<ul>
<li>등록된 Dialogue 컴포넌트를 조회 → 대화 트리의 루트 노드에서 대화 시작</li>
</ul>
</li>
</ul>
</li>
<li>확장 시 이점<ul>
<li>트리 구조나 대화 데이터 포맷이 바뀌더라도 <code>HandleActionByType(DialogueComponent)</code> 호출부는 변경 없이 유지 가능</li>
<li>대화 로직 복잡도가 커져도, NPC 베이스/외부 호출부와 분리되어 유지보수 부담 감소</li>
</ul>
</li>
</ul>
<hr>
<h2 id="마치며">마치며</h2>
<p>이번 NPC 설계에서는 <strong>Actor + Interface + Component + Enum</strong>을 조합하여, NPC의 다양한 기능을 느슨하게 결합된 형태로 구성해 보았습니다. 특히 <code>HandleActionByType()</code>과 <code>ECMNpcComponentType</code>을 활용하여 컴포넌트에 접근하는 방식은, 이후 Dialogue, Quest, Shop 컴포넌트가 추가되더라도 외부 인터페이스를 거의 건드리지 않고 확장이 가능하다는 장점이 있었습니다.</p>
<p>앞으로는 실제 트리형 Dialogue 데이터를 설계하고, 이를 처리하는 Dialogue 컴포넌트를 구현하면서, 이번에 정의한 NPC 베이스 구조가 얼마나 유연하게 동작하는지 검증해 볼 예정입니다. 이를 통해 상호작용이 복잡해지는 RPG 스타일 게임에서도 유지보수성과 확장성을 동시에 확보하는 NPC 아키텍처를 완성하는 것을 목표로 삼겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project Arc] Steam Online Service를 이용한 세션 생성 및 참여 구현 과정 (v5.6.1)]]></title>
            <link>https://velog.io/@dev_sensational/Project-Arc-5.6.1-Steam-Online-Service%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%84%B8%EC%85%98-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%B0%B8%EC%97%AC-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@dev_sensational/Project-Arc-5.6.1-Steam-Online-Service%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%84%B8%EC%85%98-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%B0%B8%EC%97%AC-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Fri, 28 Nov 2025 05:23:48 GMT</pubDate>
            <description><![CDATA[<p>Steam Online Subsystem을 사용해 친구 초대 및 멀티플레이 기능을 구현하던 중, 세션 생성과 참가(JoinSession)는 정상적으로 콜백이 오는데 실제로는 접속이 되지 않는 문제가 발생하였습니다. 특히 <code>NetworkDriver</code> 생성 실패, <code>Listen</code> 서버 생성 실패, P2P 연결 타임아웃 등 다양한 로그 메시지가 출력되면서 원인을 추적하는 데 많은 시간이 걸렸습니다.</p>
<p>이 글에서는 문제를 해결하기까지의 시행착오 과정을 정리하여, 비슷한 이슈를 겪는 분들께 참고가 될 만한 내용을 공유드리고자 합니다.</p>
<hr>
<h3 id="11-문제-상황-요약">1.1 문제 상황 요약</h3>
<ul>
<li>Steam 초대 수락 후 <code>JoinSession</code> 까지는 성공 로그가 뜸</li>
<li>하지만 실제로는 호스트에 접속되지 않고, 일정 시간이 지난 후 타임아웃</li>
<li>클라이언트 로그 상 주요 현상:<ul>
<li><code>OnSessionUserInviteAccepted</code> 성공</li>
<li><code>JoinSession</code> 성공</li>
<li><code>Browse: steam.7656.../Game/Maps/MainMenu</code> 까지 진행</li>
<li>이후 <code>Your connection to the host has been lost.</code> 로 실패</li>
</ul>
</li>
</ul>
<h3 id="12-최초-에러-networkdriver-생성-실패">1.2 최초 에러: NetworkDriver 생성 실패</h3>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/e9e53c93-23a5-409c-ba62-4c341aab70eb/image.png" alt=""></p>
<ul>
<li>로그 예시</li>
</ul>
<blockquote>
</blockquote>
<pre><code>Failed to find object &#39;Class /Script/Engine.IpNetDriver&#39;
CreateNamedNetDriver failed to create driver from definition GameNetDriver
Error initializing the pending net driver. Check the configuration of NetDriverDefinitions...</code></pre><ul>
<li>원인 추정<ul>
<li><code>DefaultEngine.ini</code> 에서 <code>GameNetDriver</code> 설정이 제대로 되어 있지 않아서, 엔진이 <code>NetDriver</code> 클래스를 찾지 못함</li>
</ul>
</li>
</ul>
<h4 id="121-netdriver-설정-수정">1.2.1 NetDriver 설정 수정</h4>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/5ec188c6-da20-4227-ba76-adc479dbdc6e/image.png" alt=""></p>
<ul>
<li><code>DefaultEngine.ini</code> 에 다음과 같이 설정<ul>
<li><code>[/Script/Engine.GameEngine]</code></li>
<li><code>!NetDriverDefinitions=ClearArray</code></li>
<li><code>NetDriverDefinitions=(DefName=&quot;GameNetDriver&quot;,DriverClassName=&quot;/Script/OnlineSubsystemSteam.SteamNetDriver&quot;,DriverClassNameFallback=&quot;/Script/OnlineSubsystemUtils.IpNetDriver&quot;)</code></li>
</ul>
</li>
<li>효과<ul>
<li>이후 로그에서 <code>InitBase PendingNetDriver (NetDriverDefinition GameNetDriver)</code> 로 정상 초기화되는 것을 확인</li>
<li>더 이상 <code>IpNetDriver</code> 클래스를 못 찾는 에러는 발생하지 않음</li>
</ul>
</li>
</ul>
<h3 id="13-두-번째-문제-p2p-연결-타임아웃">1.3 두 번째 문제: P2P 연결 타임아웃</h3>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/74ad18f6-834a-446a-a7e0-99125c9b0c6e/image.png" alt=""></p>
<ul>
<li>수정 후 클라이언트 로그<ul>
<li><code>InitBase PendingNetDriver (NetDriverDefinition GameNetDriver)</code></li>
<li><code>Game client on port 7777, rate 100000</code></li>
<li>잠시 대기 후</li>
<li><code>Timed out attempting to connect</code></li>
<li><code>Your connection to the host has been lost.</code></li>
</ul>
</li>
<li>특징<ul>
<li>세션 검색, 초대, JoinSession 까지는 모두 정상 동작</li>
<li>실제 P2P 연결 수립 단계에서 일정 시간 후 타임아웃 발생</li>
</ul>
</li>
</ul>
<h3 id="14-listen-서버-생성-실패-로그-분석">1.4 Listen 서버 생성 실패 로그 분석</h3>
<ul>
<li>호스트 쪽 로그에서 다음 메시지 확인<ul>
<li><code>LoadMap: failed to Listen(/Game/MainStage/L_GenerateMapTest?Name=Player?listen)</code></li>
</ul>
</li>
<li>다른 시점에는<ul>
<li><code>LoadMap: failed to Listen(/Game/Maps/MainMenu?Name=Player?listen)</code></li>
</ul>
</li>
<li>의미<ul>
<li>호스트가 맵을 로드하면서 <code>?listen</code> 옵션으로 Listen 서버를 열려고 시도했으나 실패</li>
<li>즉, 클라이언트 문제 이전에 <strong>호스트가 제대로 Listen 서버를 띄우지 못함</strong></li>
</ul>
</li>
</ul>
<h4 id="141-세션-생성과-travel-순서">1.4.1 세션 생성과 Travel 순서</h4>
<ul>
<li>초기 구현<ul>
<li><code>OnCreateSessionComplete()</code> 안에서 바로 <code>ServerTravel</code> + <code>?listen</code> 수행</li>
<li>또는 세션 생성 직후 약간의 딜레이를 준 뒤 <code>ServerTravel</code> 호출</li>
</ul>
</li>
<li>문제점<ul>
<li>세션 생성 직후 엔진/Steam 쪽 초기화가 완전히 끝나지 않은 상태에서 <code>ServerTravel</code> 을 수행하면 Listen 서버 생성이 실패할 수 있음</li>
<li>다만 이 경우에는 궁극적인 원인은 아니었음 (딜레이를 줘도 근본 문제가 남아 있었음)</li>
</ul>
</li>
</ul>
<h4 id="142-기능-분리">1.4.2 기능 분리</h4>
<ul>
<li>조치 사항<ul>
<li>&quot;Online Session 생성&quot; 과 &quot;ServerTravel(맵 이동 + ?listen)&quot; 을 별도의 함수로 분리</li>
<li>세션 생성 성공 콜백에서는 세션 관련 처리만 담당</li>
<li>맵 이동은 명시적으로 별도 타이밍에 호출하도록 구조 개선</li>
</ul>
</li>
<li>의도<ul>
<li>디버깅 편의성 향상</li>
<li>세션/네트 워킹 문제와 레벨 로딩/게임 로직을 명확하게 분리</li>
</ul>
</li>
</ul>
<h3 id="15-게임-타겟-vs-클라이언트-타겟">1.5 게임 타겟 vs 클라이언트 타겟</h3>
<ul>
<li>핵심 원인<ul>
<li>프로젝트를 <strong>Client 타겟(CrimsonMoonClient)</strong> 으로 빌드하고 실행하고 있었음</li>
<li>Client 타겟은 기본적으로 <strong>Listen 서버를 열 수 없는 구성</strong></li>
</ul>
</li>
<li>결과적으로<ul>
<li>호스트를 Client 타겟 실행 파일로 실행 → <code>?listen</code> 으로 서버를 열려고 해도 계속 실패</li>
<li>따라서 클라이언트 입장에서는 P2P 연결을 시도하나, 실제로는 열려 있는 Listen 서버가 없어서 결국 타임아웃 발생</li>
</ul>
</li>
</ul>
<h4 id="151-해결책-game-타겟으로-빌드">1.5.1 해결책: Game 타겟으로 빌드</h4>
<ul>
<li><p>빌드 대상 변경</p>
<ul>
<li>기존: <code>CrimsonMoonClient.Target.cs</code> 기반 빌드 (클라이언트 전용)</li>
<li>변경: <code>CrimsonMoon.Target.cs</code> 기반 <strong>Game 타겟</strong> 빌드</li>
</ul>
</li>
<li><p>변경 후 현상</p>
<ul>
<li>호스트가 <code>?listen</code> 으로 정상적으로 Listen 서버를 생성</li>
<li>클라이언트에서 Steam 초대 수락 → <code>JoinSession</code> → <code>Browse steam.xxx/Map</code> → 정상 접속</li>
<li>실제로 친구 초대 후 함께 플레이 가능한 상태 확인</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_sensational/post/fd1d43a9-9e69-4399-a009-13b36db6a29f/image.png" alt=""></p>
</li>
</ul>
<h3 id="16-정리-왜-client-타겟으로는-안-되었는가">1.6 정리: 왜 Client 타겟으로는 안 되었는가</h3>
<ul>
<li>Client 타겟의 특징 (언리얼 빌드 관점 가정)<ul>
<li>전용 클라이언트 실행용, 서버 관련 일부 기능이 비활성화/제한될 수 있음</li>
<li>Listen 서버(클라이언트 + 서버 겸용)를 여는 패턴에는 적합하지 않음</li>
</ul>
</li>
<li>Listen 서버 패턴<ul>
<li>한 플레이어가 <strong>호스트이자 클라이언트</strong> 역할을 하면서 <code>?listen</code> 으로 서버를 열고, 나머지는 그 호스트에 접속하는 구조</li>
<li>이 패턴은 Game 타겟(또는 Editor)에서 실행해야 정상 동작</li>
</ul>
</li>
</ul>
<hr>
<h2 id="2-배운-점-정리-til">2. 배운 점 정리 (TIL)</h2>
<h3 id="21-steam--ue5-멀티플레이-기본-흐름">2.1 Steam + UE5 멀티플레이 기본 흐름</h3>
<ul>
<li>세션 생성<ul>
<li>Steam Online Subsystem 이용</li>
<li><code>CreateSession</code> → <code>OnCreateSessionComplete</code> 콜백 확인</li>
</ul>
</li>
<li>호스트 측 레벨 이동<ul>
<li>Listen 서버를 사용할 경우: <code>/Game/Map/YourMap?listen</code> 으로 <code>ServerTravel</code></li>
</ul>
</li>
<li>클라이언트 초대 및 Join<ul>
<li>Steam 초대 → <code>OnSessionUserInviteAccepted</code> 콜백</li>
<li><code>JoinSession</code> 호출, 성공 시 <code>GetResolvedConnectString</code> 으로 주소 획득</li>
<li><code>ClientTravel</code> 로 <code>steam.XXX/Map</code> 주소로 이동</li>
</ul>
</li>
</ul>
<h3 id="22-설정코드-레벨에서의-체크리스트">2.2 설정/코드 레벨에서의 체크리스트</h3>
<ul>
<li><code>DefaultEngine.ini</code><ul>
<li><code>[/Script/Engine.GameEngine]</code></li>
<li><code>!NetDriverDefinitions=ClearArray</code></li>
<li><code>NetDriverDefinitions=(DefName=&quot;GameNetDriver&quot;, DriverClassName=&quot;/Script/OnlineSubsystemSteam.SteamNetDriver&quot;, DriverClassNameFallback=&quot;/Script/OnlineSubsystemUtils.IpNetDriver&quot;)</code></li>
</ul>
</li>
<li>세션 생성과 맵 이동 분리<ul>
<li>세션 생성 전용 함수</li>
<li>Listen 서버용 <code>ServerTravel</code> 전용 함수</li>
</ul>
</li>
<li>Host 빌드 타겟<ul>
<li><strong>반드시 Game 타겟(CrimsonMoon.exe)</strong> 로 빌드/실행</li>
<li>Client 전용 타겟(CrimsonMoonClient.exe)으로는 Listen 서버를 열 수 없음</li>
</ul>
</li>
</ul>
<h3 id="23-로그를-볼-때-집중해서-봐야-하는-포인트">2.3 로그를 볼 때 집중해서 봐야 하는 포인트</h3>
<ul>
<li>NetDriver 관련<ul>
<li><code>CreateNamedNetDriver failed to create driver from definition GameNetDriver</code></li>
<li><code>Failed to find object &#39;Class /Script/Engine.IpNetDriver&#39;</code></li>
</ul>
</li>
<li>Listen 서버 관련<ul>
<li><code>LoadMap: failed to Listen( ... ?listen)</code></li>
</ul>
</li>
<li>연결 실패 관련<ul>
<li><code>Timed out attempting to connect</code></li>
<li><code>Your connection to the host has been lost.</code></li>
</ul>
</li>
<li>Steam Sockets 관련 경고<ul>
<li><code>Relay candidates enabled by P2P_Transport_ICE_Enable, but P2P_TURN_ServerList is empty</code></li>
<li>단순 경고일 수 있으며, 치명적인 에러는 아닐 수 있음 → 진짜 실패 원인은 NetDriver/Listen 쪽에 있을 가능성이 높음</li>
</ul>
</li>
</ul>
<h3 id="24-앞으로-비슷한-기능을-구현할-때-주의할-점">2.4 앞으로 비슷한 기능을 구현할 때 주의할 점</h3>
<ul>
<li>먼저 <strong>호스트가 제대로 Listen 서버를 뜨는지</strong> 단독 실행으로 확인</li>
<li>클라이언트/게임 타겟, 서버 타겟 빌드 설정을 명확하게 구분</li>
<li>Online Subsystem 설정(<code>DefaultEngine.ini</code>) 과 NetDriver 설정을 초기 단계에서 정확히 맞춰둘 것</li>
<li>세션 로직과 게임 레벨 로딩/게임플레이 초기화 로직을 분리해서 디버깅 가능하게 설계</li>
</ul>
<hr>
<h2 id="결론">결론</h2>
<p>이번 구현의 핵심은 코드나 Steam API 사용법 자체의 문제라기보다는, <strong>빌드 타겟 선택(Game vs Client)</strong> 과 <strong>엔진 설정(NetDriver, Listen 서버 가능 여부)</strong> 이었습니다. 언리얼 멀티플레이를 Steam과 연동해서 구현할 때는, 에디터에서 잘 되던 것이 패키징 후 동작하지 않을 수 있으므로, 어떤 타겟으로 빌드하고 실행하는지부터 확실히 인지하는 것이 중요하다는 점을 다시 한 번 느꼈습니다.</p>
<p>이 TIL이 이후에 비슷한 문제를 겪을 때 빠르게 원인을 좁히는 데 도움이 되기를 바랍니다.</p>
]]></description>
        </item>
    </channel>
</rss>