<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Client Side</title>
    <link>https://moonsj50.tistory.com/</link>
    <description>UE5 C++ 클라이언트 개발자를 향한 기록 | 언리얼엔진 &amp;middot; 모바일 최적화 &amp;middot; AI 활용 개발</description>
    <language>ko</language>
    <pubDate>Thu, 11 Jun 2026 10:23:14 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Client Side</managingEditor>
    <image>
      <title>Client Side</title>
      <url>https://tistory1.daumcdn.net/tistory/8138256/attach/3e1e647ad18f4be291d7215b4f33b191</url>
      <link>https://moonsj50.tistory.com</link>
    </image>
    <item>
      <title>[AFO_Refactor #1] Critical 버그 수정 - AnimNotify 권한 누락, Aurora 오타, 팀 비교 반전</title>
      <link>https://moonsj50.tistory.com/219</link>
      <description>&lt;!-- 티스토리 HTML 모드에 붙여넣기 --&gt;

&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; line-height: 1.8; color: #333;&quot;&gt;

  &lt;!-- 헤더 --&gt;
  &lt;div style=&quot;background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); padding: 40px 30px; border-radius: 12px; margin-bottom: 30px;&quot;&gt;
    &lt;p style=&quot;color: #e94560; font-size: 14px; margin: 0 0 8px 0; letter-spacing: 2px;&quot;&gt;AFO_Refactor 개발일지 #1&lt;/p&gt;
    &lt;h1 style=&quot;color: #ffffff; font-size: 26px; margin: 0 0 12px 0; font-weight: 700;&quot;&gt;Critical 버그 수정&lt;/h1&gt;
    &lt;p style=&quot;color: #a8b2d8; font-size: 14px; margin: 0;&quot;&gt;AnimNotify 권한 누락 · Aurora 오타 · 팀 비교 반전 · BeginPlay 중복 호출 · 인코딩 변환&lt;/p&gt;
  &lt;/div&gt;

  &lt;!-- 개요 --&gt;
  &lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot;&gt;  이번 작업 목표&lt;/h2&gt;

  &lt;p&gt;#0에서 분석한 버그 목록 중 &lt;strong&gt;코드 수정으로 즉시 해결 가능한 것들&lt;/strong&gt;을 먼저 정리했다. GAS 도입 전에 기존 코드의 명백한 오류들을 제거해야 리팩토링 기반이 안정적으로 잡힌다.&lt;/p&gt;

  &lt;p&gt;이번에 수정한 항목은 총 5개다.&lt;/p&gt;

  &lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background: #1a1a2e; color: white;&quot;&gt;
        &lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;#&lt;/th&gt;
        &lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;버그&lt;/th&gt;
        &lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;등급&lt;/th&gt;
        &lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;파일&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;1&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;AnimNotify_AttackHit HasAuthority 누락&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff4757; font-weight: 700;&quot;&gt;  Critical&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;&lt;code&gt;AFAnimInstance.cpp&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;2&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;AFAurora::StopSprint Super 오타&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff4757; font-weight: 700;&quot;&gt;  Critical&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;&lt;code&gt;AFAurora.cpp&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;3&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;Multicast_NotifyDamage 팀 비교 반전&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff9f43; font-weight: 700;&quot;&gt;  High&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;&lt;code&gt;AFAttributeComponent.cpp&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;4&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;BeginPlay InitializeCharacterData 중복 호출&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff9f43; font-weight: 700;&quot;&gt;  High&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;&lt;code&gt;AFPlayerCharacter.cpp&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;5&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;소스 파일 인코딩 CP949 → UTF-8 with BOM&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #f39c12; font-weight: 700;&quot;&gt;  Medium&lt;/td&gt;
        &lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;33개 파일&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;!-- Fix 1 --&gt;
  &lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot;&gt;  Fix 1 — AnimNotify_AttackHit HasAuthority 누락&lt;/h2&gt;

  &lt;h3 style=&quot;font-size: 16px; color: #555; margin-top: 20px;&quot;&gt;문제&lt;/h3&gt;
  &lt;p&gt;AnimNotify는 애니메이션이 재생되는 &lt;strong&gt;모든 클라이언트에서 발동&lt;/strong&gt;된다. 멀티플레이에서 캐릭터 애니메이션은 Multicast로 모든 클라이언트에 복제되기 때문에, AnimNotify도 서버 + 모든 클라이언트에서 동시에 실행된다.&lt;/p&gt;
  &lt;p&gt;그런데 기존 코드는 권한 체크 없이 바로 &lt;code&gt;DealDamage()&lt;/code&gt;를 호출하고 있었다.&lt;/p&gt;

  &lt;div style=&quot;background: #1e1e1e; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;color: #e94560; font-size: 12px; margin: 0 0 8px 0; letter-spacing: 1px;&quot;&gt;BEFORE&lt;/p&gt;
    &lt;pre style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;void UAFAnimInstance::AnimNotify_AttackHit()
{
    if (auto Owner = TryGetPawnOwner())
    {
        if (auto Character = Cast&amp;lt;AAFPlayerCharacter&amp;gt;(Owner))
        {
            &lt;span style=&quot;color: #ff6b6b;&quot;&gt;Character-&gt;DealDamage();&lt;/span&gt;  &lt;span style=&quot;color: #6a9955;&quot;&gt;// HasAuthority() 체크 없음!&lt;/span&gt;
        }
    }
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;h3 style=&quot;font-size: 16px; color: #555; margin-top: 20px;&quot;&gt;원인&lt;/h3&gt;
  &lt;p&gt;&lt;code&gt;DealDamage()&lt;/code&gt;는 내부에 &lt;code&gt;HasAuthority()&lt;/code&gt; 가드가 있지만, 클라이언트에서 호출 자체가 되면 Dedicated Server 환경에서 불필요한 실행 경로가 생긴다. 특히 &lt;code&gt;DealDamage()&lt;/code&gt; 내부 가드가 없는 버전이 혼재할 경우 크래시로 이어진다.&lt;/p&gt;
  &lt;p&gt;근본 원칙은 &lt;strong&gt;&quot;서버 전용 함수는 진입부에서 차단&quot;&lt;/strong&gt;이다. 내부에서 막는 것보다 외부에서 먼저 막는 게 안전하다.&lt;/p&gt;

  &lt;div style=&quot;background: #1e1e1e; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;color: #4caf50; font-size: 12px; margin: 0 0 8px 0; letter-spacing: 1px;&quot;&gt;AFTER&lt;/p&gt;
    &lt;pre style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;void UAFAnimInstance::AnimNotify_AttackHit()
{
    APawn* Owner = TryGetPawnOwner();
    if (!Owner) return;
    &lt;span style=&quot;color: #4caf50;&quot;&gt;if (!Owner-&gt;HasAuthority()) return;&lt;/span&gt;  &lt;span style=&quot;color: #6a9955;&quot;&gt;// 서버에서만 실행&lt;/span&gt;

    AAFPlayerCharacter* Character = Cast&amp;lt;AAFPlayerCharacter&amp;gt;(Owner);
    if (!Character) return;

    Character-&gt;DealDamage();
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;!-- Fix 2 --&gt;
  &lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot;&gt;  Fix 2 — AFAurora::StopSprint Super 오타&lt;/h2&gt;

  &lt;h3 style=&quot;font-size: 16px; color: #555; margin-top: 20px;&quot;&gt;문제&lt;/h3&gt;
  &lt;p&gt;오로라 캐릭터의 &lt;code&gt;StopSprint()&lt;/code&gt;에서 부모 함수를 잘못 호출하고 있었다. &lt;code&gt;Super::StopSprint&lt;/code&gt;를 호출해야 하는데 &lt;code&gt;Super::StartSprint&lt;/code&gt;를 호출하고 있었다.&lt;/p&gt;

  &lt;div style=&quot;background: #1e1e1e; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;color: #e94560; font-size: 12px; margin: 0 0 8px 0; letter-spacing: 1px;&quot;&gt;BEFORE&lt;/p&gt;
    &lt;pre style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;void AAFAurora::StopSprint(const FInputActionValue&amp; Value)
{
    &lt;span style=&quot;color: #ff6b6b;&quot;&gt;Super::StartSprint(Value);&lt;/span&gt;  &lt;span style=&quot;color: #6a9955;&quot;&gt;// ← StopSprint인데 StartSprint 호출&lt;/span&gt;
    bIsSprinting = false;
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;h3 style=&quot;font-size: 16px; color: #555; margin-top: 20px;&quot;&gt;결과&lt;/h3&gt;
  &lt;p&gt;스프린트 키를 떼면 부모의 &lt;code&gt;StartSprint&lt;/code&gt;가 실행된다. &lt;code&gt;StartSprint&lt;/code&gt;는 &lt;code&gt;MaxWalkSpeed&lt;/code&gt;를 스프린트 속도로 올리는 함수이므로, 키를 떼도 빠른 속도가 유지된다. &lt;code&gt;bIsSprinting = false&lt;/code&gt;로 플래그만 내려가고 실제 이동 속도는 복원되지 않는 버그.&lt;/p&gt;

  &lt;div style=&quot;background: #1e1e1e; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;color: #4caf50; font-size: 12px; margin: 0 0 8px 0; letter-spacing: 1px;&quot;&gt;AFTER&lt;/p&gt;
    &lt;pre style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;void AAFAurora::StopSprint(const FInputActionValue&amp; Value)
{
    &lt;span style=&quot;color: #4caf50;&quot;&gt;Super::StopSprint(Value);&lt;/span&gt;  &lt;span style=&quot;color: #6a9955;&quot;&gt;// 수정&lt;/span&gt;
    bIsSprinting = false;
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;div style=&quot;background: #fff3cd; border-left: 4px solid #f39c12; padding: 16px 20px; border-radius: 0 8px 8px 0; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;margin: 0; font-size: 14px;&quot;&gt;  &lt;strong&gt;교훈:&lt;/strong&gt; 캐릭터 파생 클래스가 늘어날수록 이런 복사-붙여넣기 오타가 발생하기 쉽다. 이 문제는 이후 포스팅에서 다룰 &lt;strong&gt;Interface 도입&lt;/strong&gt;으로 근본적으로 해결할 예정이다. bIsSprinting을 베이스 클래스로 올리면 파생 클래스에서 이 함수를 override할 필요 자체가 없어진다.&lt;/p&gt;
  &lt;/div&gt;

  &lt;!-- Fix 3 --&gt;
  &lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot;&gt;  Fix 3 — Multicast_NotifyDamage 팀 비교 반전&lt;/h2&gt;

  &lt;h3 style=&quot;font-size: 16px; color: #555; margin-top: 20px;&quot;&gt;문제&lt;/h3&gt;
  &lt;p&gt;데미지 수치 UI의 색상이 반대로 표시되고 있었다. 적을 때리면 아군 색상, 아군을 때리면 적 색상으로 뜨는 버그.&lt;/p&gt;

  &lt;div style=&quot;background: #1e1e1e; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;color: #e94560; font-size: 12px; margin: 0 0 8px 0; letter-spacing: 1px;&quot;&gt;BEFORE&lt;/p&gt;
    &lt;pre style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;span style=&quot;color: #6a9955;&quot;&gt;// 같은 팀 공격 = 적 데미지로 표시 (반전)&lt;/span&gt;
if (MyPS-&gt;GetTeamID() &lt;span style=&quot;color: #ff6b6b;&quot;&gt;==&lt;/span&gt; InstigatorPS-&gt;GetTeamID())
{
    bIsEnemyDamage = true;
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;div style=&quot;background: #1e1e1e; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;color: #4caf50; font-size: 12px; margin: 0 0 8px 0; letter-spacing: 1px;&quot;&gt;AFTER&lt;/p&gt;
    &lt;pre style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;span style=&quot;color: #6a9955;&quot;&gt;// 다른 팀(적) 공격 = 적 데미지로 표시 (정상)&lt;/span&gt;
if (MyPS-&gt;GetTeamID() &lt;span style=&quot;color: #4caf50;&quot;&gt;!=&lt;/span&gt; InstigatorPS-&gt;GetTeamID())
{
    bIsEnemyDamage = true;
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;!-- Fix 4 --&gt;
  &lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot;&gt;  Fix 4 — BeginPlay InitializeCharacterData 중복 호출&lt;/h2&gt;

  &lt;h3 style=&quot;font-size: 16px; color: #555; margin-top: 20px;&quot;&gt;문제&lt;/h3&gt;
  &lt;p&gt;팀 작업 중 병합 과정에서 동일한 블록이 두 번 들어간 것으로 추정된다. DataTable에서 캐릭터 데이터를 로드하는 함수가 &lt;code&gt;BeginPlay()&lt;/code&gt;에서 두 번 실행됐다.&lt;/p&gt;

  &lt;div style=&quot;background: #1e1e1e; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;color: #e94560; font-size: 12px; margin: 0 0 8px 0; letter-spacing: 1px;&quot;&gt;BEFORE&lt;/p&gt;
    &lt;pre style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;void AAFPlayerCharacter::BeginPlay()
{
    Super::BeginPlay();

    if (!CharacterKey.IsNone())
    {
        InitializeCharacterData(CharacterKey.ToString());  &lt;span style=&quot;color: #6a9955;&quot;&gt;// 1번째&lt;/span&gt;
    }

    &lt;span style=&quot;color: #ff6b6b;&quot;&gt;if (!CharacterKey.IsNone())
    {
        InitializeCharacterData(CharacterKey.ToString());  // 2번째 (중복)
    }&lt;/span&gt;
    ...
}&lt;/pre&gt;
  &lt;/div&gt;

  &lt;h3 style=&quot;font-size: 16px; color: #555; margin-top: 20px;&quot;&gt;결과&lt;/h3&gt;
  &lt;p&gt;DataTable 로드가 두 번 실행되면서 &lt;code&gt;CharacterSkills&lt;/code&gt; 배열이 &lt;code&gt;Empty()&lt;/code&gt; 후 다시 채워지는 과정이 반복된다. 부작용이 숨어있는 코드다. 두 번째 블록을 제거해서 정리했다.&lt;/p&gt;

  &lt;!-- Fix 5 --&gt;
  &lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot;&gt;  Fix 5 — 소스 파일 인코딩 CP949 → UTF-8 with BOM&lt;/h2&gt;

  &lt;h3 style=&quot;font-size: 16px; color: #555; margin-top: 20px;&quot;&gt;문제&lt;/h3&gt;
  &lt;p&gt;팀원들이 Windows 한국어 환경에서 작성한 파일들이 CP949(EUC-KR 계열) 인코딩으로 저장되어 있었다. BOM이 없는 상태라 UTF-8 기반 도구에서 한글 주석이 모두 깨져 보였다.&lt;/p&gt;

  &lt;div style=&quot;background: #1e1e1e; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;pre style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;span style=&quot;color: #ff6b6b;&quot;&gt;// ❌ CP949 - UTF-8 도구에서 깨짐&lt;/span&gt;
&lt;span style=&quot;color: #ff6b6b;&quot;&gt;// ¼­¹ö Àü¿ë: ½Ã°£ ¼³Á¤&lt;/span&gt;
void AAFGameState::SetRemainingTime(int32 NewTime)&lt;/pre&gt;
  &lt;/div&gt;

  &lt;h3 style=&quot;font-size: 16px; color: #555; margin-top: 20px;&quot;&gt;해결&lt;/h3&gt;
  &lt;p&gt;총 &lt;strong&gt;33개 파일&lt;/strong&gt;(15 .cpp + 18 .h)을 UTF-8 with BOM으로 일괄 변환했다. UE5 MSVC 환경은 UTF-8 with BOM을 표준으로 처리하므로 컴파일에 영향 없다.&lt;/p&gt;

  &lt;div style=&quot;background: #f0f4ff; border-radius: 8px; padding: 16px 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;margin: 0; font-size: 14px;&quot;&gt;  &lt;strong&gt;UTF-8 with BOM vs UTF-8 without BOM&lt;/strong&gt;&lt;br&gt;
    UE5 C++ 프로젝트에서는 BOM이 있는 UTF-8을 권장한다. BOM이 없으면 MSVC가 시스템 코드페이지(한국: CP949)로 파일을 해석해 한글이 깨질 수 있다.&lt;/p&gt;
  &lt;/div&gt;

  &lt;!-- 배운 것 --&gt;
  &lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot;&gt;  이번 작업에서 배운 것&lt;/h2&gt;

  &lt;div style=&quot;background: #f0f4ff; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;margin: 0 0 12px 0;&quot;&gt;&lt;strong&gt;AnimNotify의 실행 컨텍스트를 항상 의식해야 한다.&lt;/strong&gt;&lt;/p&gt;
    &lt;p style=&quot;margin: 0; font-size: 14px; color: #555;&quot;&gt;AnimNotify는 애니메이션이 재생되는 모든 머신에서 실행된다. Multicast로 복제된 몽타주라면 서버 + 전체 클라이언트에서 동시에 발동된다. 게임 로직(데미지, 스폰 등)은 반드시 &lt;code&gt;HasAuthority()&lt;/code&gt;로 서버에서만 처리해야 한다.&lt;/p&gt;
  &lt;/div&gt;

  &lt;div style=&quot;background: #f0f4ff; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
    &lt;p style=&quot;margin: 0 0 12px 0;&quot;&gt;&lt;strong&gt;팀 프로젝트 코드에는 병합 잔재가 남아있다.&lt;/strong&gt;&lt;/p&gt;
    &lt;p style=&quot;margin: 0; font-size: 14px; color: #555;&quot;&gt;중복 호출, 오타, 인코딩 문제는 모두 팀 작업 과정에서 발생한 전형적인 잔재다. 리팩토링 첫 단계에서 이런 것들을 먼저 정리해야 이후 구조 개선 작업이 안정적으로 진행된다.&lt;/p&gt;
  &lt;/div&gt;

  &lt;!-- 다음 포스팅 --&gt;
  &lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot;&gt;  다음 포스팅&lt;/h2&gt;

  &lt;div style=&quot;background: #1a1a2e; border-radius: 8px; padding: 20px; color: white;&quot;&gt;
    &lt;p style=&quot;margin: 0; font-size: 15px;&quot;&gt;&lt;strong&gt;[AFO_Refactor #2]&lt;/strong&gt; Cast 체인 → Interface 교체&lt;/p&gt;
    &lt;p style=&quot;margin: 8px 0 0 0; font-size: 13px; color: #a8b2d8;&quot;&gt;매 프레임 DarkKnight/Mage/Aurora Cast 3번을 Interface로 교체. bIsSprinting을 베이스 클래스로 올려서 파생 클래스 override 자체를 제거하는 구조 개선.&lt;/p&gt;
  &lt;/div&gt;

&lt;/div&gt;</description>
      <category>개인프로젝트/AFO_Refactor</category>
      <category>AnimNotify</category>
      <category>dedicated server</category>
      <category>HasAuthority</category>
      <category>ue5</category>
      <category>게임개발</category>
      <category>리팩토링</category>
      <category>멀티플레이</category>
      <category>언리얼엔진</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/219</guid>
      <comments>https://moonsj50.tistory.com/219#entry219comment</comments>
      <pubDate>Wed, 10 Jun 2026 17:17:22 +0900</pubDate>
    </item>
    <item>
      <title>C++ STL stack / queue 완전 정리 &amp;mdash; LIFO vs FIFO 실전편</title>
      <link>https://moonsj50.tistory.com/218</link>
      <description>&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; max-width: 720px; line-height: 1.8; color: #1f2328;&quot;&gt;

&lt;!-- 헤더 --&gt;
&lt;div style=&quot;border-left: 4px solid #0969da; padding-left: 16px; margin-bottom: 32px;&quot;&gt;
  &lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0 0 4px;&quot; data-ke-size=&quot;size16&quot;&gt;코딩테스트 준비 #2&lt;/p&gt;
  &lt;h1 style=&quot;font-size: 24px; font-weight: bold; margin: 0 0 4px; color: #1f2328;&quot;&gt;C++ STL stack / queue 완전 정리 — LIFO vs FIFO 실전편&lt;/h1&gt;
  &lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;STL · stack · queue · 프로그래머스 · 올바른 괄호&lt;/p&gt;
&lt;/div&gt;

&lt;!-- 목표 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 32px;&quot; data-ke-size=&quot;size26&quot;&gt;  목표&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;코딩테스트 스택/큐 유형을 C++로 풀기 위해 &lt;b&gt;stack&lt;/b&gt;과 &lt;b&gt;queue&lt;/b&gt; 기본 문법을 정리하고, 프로그래머스 &lt;b&gt;올바른 괄호(Level 2)&lt;/b&gt; 문제에 직접 적용한다.&lt;/p&gt;

&lt;!-- stack vs queue --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  stack vs queue vs deque&lt;/h2&gt;

&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
  &lt;tbody&gt;
    &lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;STL&lt;/b&gt;&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;구조&lt;/b&gt;&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;접근 방향&lt;/b&gt;&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;범위기반 for&lt;/b&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;stack&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;LIFO&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;한쪽 끝만 (top)&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #cf222e; border-bottom: 1px solid #d0d7de;&quot;&gt;❌&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
      &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;queue&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;FIFO&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;앞(front) / 뒤(back)&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #cf222e; border-bottom: 1px solid #d0d7de;&quot;&gt;❌&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da;&quot;&gt;deque&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;양방향&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;앞/뒤 모두 + 인덱스&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1a7f37;&quot;&gt;✅&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;div style=&quot;background: #ddf4ff; border-left: 4px solid #0969da; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
  &lt;p style=&quot;margin: 0; color: #0550ae; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;stack과 queue는 내부적으로 &lt;b&gt;deque 기반&lt;/b&gt;이다. 접근 방향을 제한해놓은 것뿐이다.&lt;/p&gt;
&lt;/div&gt;

&lt;!-- stack --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  stack 기본 문법&lt;/h2&gt;

&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
  &lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;
    &lt;span style=&quot;color: #0969da;&quot;&gt;#include&lt;/span&gt; &amp;lt;stack&amp;gt;&lt;br/&gt;&lt;br/&gt;
    stack&amp;lt;&lt;span style=&quot;color: #0969da;&quot;&gt;int&lt;/span&gt;&amp;gt; st;&lt;br/&gt;&lt;br/&gt;
    st.push(&lt;span style=&quot;color: #cf222e;&quot;&gt;3&lt;/span&gt;);     &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 위에 추가&lt;/span&gt;&lt;br/&gt;
    st.top();      &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 맨 위 값 확인 (제거 안함)&lt;/span&gt;&lt;br/&gt;
    st.pop();      &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 맨 위 값 제거&lt;/span&gt;&lt;br/&gt;
    st.empty();    &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 비어있으면 true&lt;/span&gt;&lt;br/&gt;
    st.size();     &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 원소 개수&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
  &lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;전체 순회 방법&lt;/p&gt;
  &lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;
    &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 범위기반 for 불가 → while로 꺼내야 함&lt;/span&gt;&lt;br/&gt;
    &lt;span style=&quot;color: #0969da;&quot;&gt;while&lt;/span&gt; (!st.empty())&lt;br/&gt;
    {&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;cout &amp;lt;&amp;lt; st.top() &amp;lt;&amp;lt; &lt;span style=&quot;color: #cf222e;&quot;&gt;&quot;\n&quot;&lt;/span&gt;;&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;st.pop();&lt;br/&gt;
    }&lt;br/&gt;
    &lt;span style=&quot;color: #1a7f37;&quot;&gt;// LIFO니까 역순 출력됨&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div style=&quot;background: #fff8c5; border-left: 4px solid #d4a72c; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
  &lt;p style=&quot;margin: 0; color: #7d4e00; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;b&gt;빈 스택에서 pop() 하면 런타임 에러.&lt;/b&gt; 반드시 empty() 체크 후 pop() 해야 한다.&lt;/p&gt;
&lt;/div&gt;

&lt;!-- queue --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  queue 기본 문법&lt;/h2&gt;

&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
  &lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2338; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;
    &lt;span style=&quot;color: #0969da;&quot;&gt;#include&lt;/span&gt; &amp;lt;queue&amp;gt;&lt;br/&gt;&lt;br/&gt;
    queue&amp;lt;&lt;span style=&quot;color: #0969da;&quot;&gt;int&lt;/span&gt;&amp;gt; q;&lt;br/&gt;&lt;br/&gt;
    q.push(&lt;span style=&quot;color: #cf222e;&quot;&gt;3&lt;/span&gt;);     &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 뒤에 추가&lt;/span&gt;&lt;br/&gt;
    q.front();     &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 맨 앞 값 확인 (제거 안함)&lt;/span&gt;&lt;br/&gt;
    q.back();      &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 맨 뒤 값 확인&lt;/span&gt;&lt;br/&gt;
    q.pop();       &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 맨 앞 값 제거&lt;/span&gt;&lt;br/&gt;
    q.empty();     &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 비어있으면 true&lt;/span&gt;&lt;br/&gt;
    q.size();      &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 원소 개수&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
  &lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;전체 순회 방법&lt;/p&gt;
  &lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;
    &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 범위기반 for 불가 → while로 꺼내야 함&lt;/span&gt;&lt;br/&gt;
    &lt;span style=&quot;color: #0969da;&quot;&gt;while&lt;/span&gt; (!q.empty())&lt;br/&gt;
    {&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;cout &amp;lt;&amp;lt; q.front() &amp;lt;&amp;lt; &lt;span style=&quot;color: #cf222e;&quot;&gt;&quot;\n&quot;&lt;/span&gt;;&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;q.pop();&lt;br/&gt;
    }&lt;br/&gt;
    &lt;span style=&quot;color: #1a7f37;&quot;&gt;// FIFO니까 넣은 순서대로 출력됨&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;!-- stack vs queue 비교 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;⚡ stack vs queue 한눈에 비교&lt;/h2&gt;

&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
  &lt;tbody&gt;
    &lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;stack&lt;/b&gt;&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;queue&lt;/b&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;추가&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;push() → 위에&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;push() → 뒤에&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;확인&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;top()&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;front() / back()&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;제거&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;pop() → 위에서&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;pop() → 앞에서&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;순서&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;LIFO (역순)&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;FIFO (입력 순서)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;코테 용도&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;괄호, DFS, 히스토리&lt;/td&gt;
      &lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;BFS, 시뮬레이션&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;!-- 실전 적용 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  실전 적용 — 올바른 괄호&lt;/h2&gt;

&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;문자열이 주어질 때, 올바른 괄호 문자열인지 판별해라. &lt;b&gt;(&lt;/b&gt; 로 열면 반드시 &lt;b&gt;)&lt;/b&gt; 로 닫혀야 한다.&lt;/p&gt;

&lt;div style=&quot;background: #ddf4ff; border-left: 4px solid #0969da; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
  &lt;p style=&quot;margin: 0; color: #0550ae; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 로직:&lt;/b&gt; '(' 나오면 push → ')' 나오면 스택 비어있으면 false, 아니면 pop → 루프 끝나고 스택에 남아있으면 false&lt;/p&gt;
&lt;/div&gt;

&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
  &lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;
    &lt;span style=&quot;color: #0969da;&quot;&gt;#include&lt;/span&gt; &amp;lt;string&amp;gt;&lt;br/&gt;
    &lt;span style=&quot;color: #0969da;&quot;&gt;#include&lt;/span&gt; &amp;lt;stack&amp;gt;&lt;br/&gt;
    &lt;span style=&quot;color: #0969da;&quot;&gt;using namespace&lt;/span&gt; std;&lt;br/&gt;&lt;br/&gt;
    &lt;span style=&quot;color: #0969da;&quot;&gt;bool&lt;/span&gt; solution(string s)&lt;br/&gt;
    {&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;bool&lt;/span&gt; answer = &lt;span style=&quot;color: #0969da;&quot;&gt;true&lt;/span&gt;;&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;stack&amp;lt;&lt;span style=&quot;color: #0969da;&quot;&gt;char&lt;/span&gt;&amp;gt; st;&lt;br/&gt;&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;for&lt;/span&gt; (&lt;span style=&quot;color: #0969da;&quot;&gt;int&lt;/span&gt; i = 0; i &amp;lt; s.size(); ++i)&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;{&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;if&lt;/span&gt; (s[i] == &lt;span style=&quot;color: #cf222e;&quot;&gt;'('&lt;/span&gt;)&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;st.push(&lt;span style=&quot;color: #cf222e;&quot;&gt;'('&lt;/span&gt;);&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;else if&lt;/span&gt; (s[i] == &lt;span style=&quot;color: #cf222e;&quot;&gt;')'&lt;/span&gt;)&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;if&lt;/span&gt; (st.empty()) &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 빈 스택에서 pop 방지&lt;/span&gt;&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;return false&lt;/span&gt;;&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;st.pop();&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;}&lt;br/&gt;&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;if&lt;/span&gt; (!st.empty()) &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 루프 끝났는데 '(' 남아있으면&lt;/span&gt;&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;{&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;answer = &lt;span style=&quot;color: #0969da;&quot;&gt;false&lt;/span&gt;;&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;}&lt;br/&gt;
    &amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;return&lt;/span&gt; answer;&lt;br/&gt;
    }
  &lt;/div&gt;
&lt;/div&gt;

&lt;!-- 배운 것 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  배운 것&lt;/h2&gt;

&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
  &lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; stack과 queue는 범위기반 for 불가. while + empty() 체크로 순회해야 한다.&lt;/p&gt;
  &lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; 빈 스택/큐에서 pop()은 런타임 에러. 반드시 empty() 체크 후 pop() 해야 한다.&lt;/p&gt;
  &lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; stack은 DFS/괄호/히스토리, queue는 BFS/시뮬레이션에 쓴다.&lt;/p&gt;
  &lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; stack과 queue는 내부적으로 deque 기반이다. deque는 양쪽 끝 접근 + 인덱스 접근이 가능하다.&lt;/p&gt;
  &lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; size_t는 부호 없는 정수라 int와 섞으면 언더플로우 버그 발생 가능. (int) 캐스팅 습관 필요.&lt;/p&gt;
&lt;/div&gt;

&lt;div style=&quot;background: #fff8c5; border-left: 4px solid #d4a72c; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
  &lt;p style=&quot;margin: 0; color: #7d4e00; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음 포스팅:&lt;/b&gt; queue 실전 문제 + sort 정렬 정리&lt;/p&gt;
&lt;/div&gt;

&lt;/div&gt;</description>
      <category>코딩테스트 준비</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/218</guid>
      <comments>https://moonsj50.tistory.com/218#entry218comment</comments>
      <pubDate>Tue, 9 Jun 2026 21:59:39 +0900</pubDate>
    </item>
    <item>
      <title>[AFO_Refactor #0] 프로젝트 셋업 + 코드 분석 - 팀 프로젝트를 GAS 기반으로 리팩토링하기</title>
      <link>https://moonsj50.tistory.com/217</link>
      <description>&lt;!-- 티스토리 HTML 모드에 붙여넣기 --&gt;
&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; line-height: 1.8; color: #333;&quot;&gt;&lt;!-- 헤더 --&gt;
&lt;div style=&quot;background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); padding: 40px 30px; border-radius: 12px; margin-bottom: 30px;&quot;&gt;
&lt;p style=&quot;color: #e94560; font-size: 14px; margin: 0 0 8px 0; letter-spacing: 2px;&quot; data-ke-size=&quot;size16&quot;&gt;AFO_Refactor 개발일지 #0&lt;/p&gt;
&lt;h1 style=&quot;color: #ffffff; font-size: 26px; margin: 0 0 12px 0; font-weight: bold;&quot;&gt;프로젝트 셋업 + 코드 분석&lt;/h1&gt;
&lt;p style=&quot;color: #a8b2d8; font-size: 14px; margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;GitHub &amp;middot; CLAUDE.md &amp;middot; 버그 9개 발견 &amp;middot; GAS 리팩토링 방향&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 왜 시작했나 --&gt;
&lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  왜 이 프로젝트를 시작했나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부트캠프 팀 프로젝트로 &lt;b&gt;2v2 멀티플레이 액션 게임 AFO&lt;/b&gt;를 만들었다. Dedicated Server 기반에 5종 캐릭터, 팀 킬 스코어 방식으로 설계했고, 패키징까지 완료했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제가 있었다.&lt;/p&gt;
&lt;ul style=&quot;padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패키징 후 멀티플레이 플레이 중 크래시 발생&lt;/li&gt;
&lt;li&gt;팀원마다 코드 스타일이 달라 구조가 통일되지 않음&lt;/li&gt;
&lt;li&gt;스킬 DataTable 연동 미완성 (호출 타이밍 문제)&lt;/li&gt;
&lt;li&gt;서버 권한 처리가 일관되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업 포트폴리오로 쓰기엔 부족했다. 그래서 &lt;b&gt;혼자 처음부터 구조를 뜯어고치기로 했다.&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background: #f8f9fa; border-left: 4px solid #e94560; padding: 16px 20px; border-radius: 0 8px 8px 0; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px;&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;AI로 [시간단축] &amp;rarr; 내가 [문제발견] &amp;rarr; 내가 [구조개선] &amp;rarr; [수치결과]&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 프로젝트 개요 --&gt;
&lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  프로젝트 개요&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; font-weight: 600; width: 160px;&quot;&gt;프로젝트명&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;AFO_Refactor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; font-weight: 600;&quot;&gt;원본&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;팀 프로젝트 AFO (2v2 멀티플레이 액션)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; font-weight: 600;&quot;&gt;엔진&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;Unreal Engine 5.5 / C++&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; font-weight: 600;&quot;&gt;네트워크&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;Dedicated Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; font-weight: 600;&quot;&gt;목표&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;GAS 기반 전투 시스템 리팩토링 + 멀티플레이 크래시 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; font-weight: 600;&quot;&gt;개발 기간&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;약 4주 (Claude Code 활용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- GitHub 세팅 --&gt;
&lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  GitHub 세팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 팀 레포는 그대로 두고, &lt;b&gt;개인 레포 AFO_Refactor를 새로 생성&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LFS는 사용하지 않는다. Content 폴더를 아예 올리지 않기 때문이다. 면접관이 보는 건 코드이고, 에셋은 저작권 문제도 있어서 소스만 관리한다.&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
&lt;pre class=&quot;gauss&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0; overflow-x: auto;&quot;&gt;&lt;code&gt;# .gitignore 핵심 항목
Content/           # 에셋 (용량 크고 저작권 문제)
Binaries/          # 컴파일 결과물
Intermediate/      # 중간 빌드 파일
Saved/             # 로그, 스크린샷
DerivedDataCache/  # 에셋 캐시&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;!-- 코드 분석 --&gt;
&lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  코드 분석 결과 &amp;mdash; 발견한 버그 9개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code로 소스 전체를 분석했다. &lt;b&gt;AI가 분석 &amp;rarr; 내가 원인 판단 &amp;rarr; 우선순위 결정&lt;/b&gt; 순서로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 코드 리뷰만으로 Critical 버그 4개를 포함해 총 9개의 문제를 발견했다.&lt;/p&gt;
&lt;!-- 버그 1 --&gt;
&lt;div style=&quot;border: 1px solid #ff4757; border-radius: 8px; padding: 20px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;color: #ff4757; font-weight: bold; margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;  Critical 1 &amp;mdash; HandleSkillHitCheck 세미콜론 버그&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 12px 0; font-size: 14px; color: #666;&quot; data-ke-size=&quot;size16&quot;&gt;위치: &lt;code&gt;AFPlayerCharacter.cpp&lt;/code&gt;&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 6px; padding: 16px; margin: 8px 0;&quot;&gt;
&lt;pre class=&quot;rust&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;code&gt;bool bHit = GetWorld()-&amp;gt;OverlapMultiByChannel(...);

if (bHit) return;  // &amp;larr; 세미콜론! 충돌 시 바로 리턴
{
    for (auto&amp;amp; Result : OverlapResults)  // &amp;larr; if와 분리된 블록, 항상 실행됨
    { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 12px 0 0 0; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 스킬이 적에게 맞아도 데미지가 전혀 들어가지 않는다. 세미콜론 하나가 전체 스킬 시스템을 무력화했다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 버그 2 --&gt;
&lt;div style=&quot;border: 1px solid #ff4757; border-radius: 8px; padding: 20px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;color: #ff4757; font-weight: bold; margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;  Critical 2 &amp;mdash; AnimNotify_AttackHit 클라이언트에서 DealDamage 직접 호출&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 12px 0; font-size: 14px; color: #666;&quot; data-ke-size=&quot;size16&quot;&gt;위치: &lt;code&gt;AFAnimInstance.cpp&lt;/code&gt;&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 6px; padding: 16px; margin: 8px 0;&quot;&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;code&gt;// ❌ AnimNotify는 모든 클라이언트에서 발동됨
void UAFAnimInstance::AnimNotify_AttackHit()
{
    // HasAuthority() 체크 없음!
    Character-&amp;gt;DealDamage();  // &amp;larr; 클라이언트에서도 실행됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 12px 0 0 0; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; Dedicated Server 환경에서 클라이언트가 서버 전용 작업을 직접 실행 &amp;rarr; 멀티플레이 크래시의 핵심 원인 중 하나.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 버그 3 --&gt;
&lt;div style=&quot;border: 1px solid #ff4757; border-radius: 8px; padding: 20px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;color: #ff4757; font-weight: bold; margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;  Critical 3 &amp;mdash; AFAurora::StopSprint 오타&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 12px 0; font-size: 14px; color: #666;&quot; data-ke-size=&quot;size16&quot;&gt;위치: &lt;code&gt;AFAurora.cpp&lt;/code&gt;&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 6px; padding: 16px; margin: 8px 0;&quot;&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;code&gt;// ❌ StopSprint인데 StartSprint 호출
void AAFAurora::StopSprint(const FInputActionValue&amp;amp; Value)
{
    Super::StartSprint(Value);  // &amp;larr; StopSprint여야 함
    bIsSprinting = false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 12px 0 0 0; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 오로라 캐릭터는 스프린트 키를 떼도 속도가 원복되지 않는다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 버그 4 --&gt;
&lt;div style=&quot;border: 1px solid #ff4757; border-radius: 8px; padding: 20px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;color: #ff4757; font-weight: bold; margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;  Critical 4 &amp;mdash; DataTable 로드 타이밍 버그&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 12px 0; font-size: 14px; color: #666;&quot; data-ke-size=&quot;size16&quot;&gt;위치: &lt;code&gt;AFMage.cpp&lt;/code&gt;, &lt;code&gt;AFDarkKnight.cpp&lt;/code&gt;, &lt;code&gt;AFWereWolf.cpp&lt;/code&gt;&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 6px; padding: 16px; margin: 8px 0;&quot;&gt;
&lt;pre class=&quot;lasso&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;code&gt;// ❌ OnRep_PlayerState 시점에 DataTable이 아직 null일 수 있음
void AAFMage::OnRep_PlayerState()
{
    LoadMageData();  // &amp;larr; SkillDataTable이 nullptr인 채로
}

void AAFMage::LoadMageData()
{
    // nullptr 체크 없이 바로 FindRow() 진입 &amp;rarr; 크래시
    SkillDataTable-&amp;gt;FindRow&amp;lt;FMageSkillData&amp;gt;(...);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 12px 0 0 0; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 클라이언트에서 스킬 아이콘이 뜨지 않거나, nullptr 역참조로 크래시. 스킬 슬롯 DataTable 연동이 끝내 안 됐던 근본 원인이다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 버그 5 --&gt;
&lt;div style=&quot;border: 1px solid #ff9f43; border-radius: 8px; padding: 20px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;color: #ff9f43; font-weight: bold; margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;  High 1 &amp;mdash; AnimInstance 매 프레임 Cast 체인&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 12px 0; font-size: 14px; color: #666;&quot; data-ke-size=&quot;size16&quot;&gt;위치: &lt;code&gt;AFAnimInstance.cpp&lt;/code&gt; &lt;code&gt;NativeUpdateAnimation()&lt;/code&gt;&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 6px; padding: 16px; margin: 8px 0;&quot;&gt;
&lt;pre class=&quot;xl&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;code&gt;// ❌ 매 프레임 Cast 3번 실행
if (AAFDarkKnight* DK = Cast&amp;lt;AAFDarkKnight&amp;gt;(BaseChar))
    bIsSprinting = DK-&amp;gt;bIsSprinting;
else if (AAFMage* Mage = Cast&amp;lt;AAFMage&amp;gt;(BaseChar))
    bIsSprinting = Mage-&amp;gt;bIsSprinting;
else if (AAFAurora* Aurora = Cast&amp;lt;AAFAurora&amp;gt;(BaseChar))
    bIsSprinting = Aurora-&amp;gt;bIsSprinting;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 12px 0 0 0; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 캐릭터 추가 시 이 함수도 수정해야 한다. 확장성 없는 구조 + 매 프레임 불필요한 Cast 비용.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 버그 6 --&gt;
&lt;div style=&quot;border: 1px solid #ff9f43; border-radius: 8px; padding: 20px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;color: #ff9f43; font-weight: bold; margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;  High 2 &amp;mdash; Multicast_NotifyDamage 팀 비교 로직 반대&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 12px 0; font-size: 14px; color: #666;&quot; data-ke-size=&quot;size16&quot;&gt;위치: &lt;code&gt;AFAttributeComponent.cpp&lt;/code&gt;&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 6px; padding: 16px; margin: 8px 0;&quot;&gt;
&lt;pre class=&quot;xl&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;code&gt;// ❌ 같은 팀 = 적 데미지로 표시되는 반전 로직
if (MyPS-&amp;gt;GetTeamID() == InstigatorPS-&amp;gt;GetTeamID())
{
    bIsEnemyDamage = true;  // &amp;larr; 같은 팀인데 적 데미지?
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 12px 0 0 0; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 아군을 공격하면 적 색상으로, 적을 공격하면 아군 색상으로 데미지 수치가 표시된다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 버그 7 --&gt;
&lt;div style=&quot;border: 1px solid #feca57; border-radius: 8px; padding: 20px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;color: #f39c12; font-weight: bold; margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;  Medium 1 &amp;mdash; UI EnsureUI 반복 타이머&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 12px 0; font-size: 14px; color: #666;&quot; data-ke-size=&quot;size16&quot;&gt;위치: &lt;code&gt;AFLobbyPlayerController.cpp&lt;/code&gt;&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 6px; padding: 16px; margin: 8px 0;&quot;&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;code&gt;// ❌ UI가 붙은 후에도 0.2초마다 계속 실행
GetWorldTimerManager().SetTimer(
    EnsureUITimer, this,
    &amp;amp;ThisClass::SetupUIForCurrentMap,
    0.2f, true  // &amp;larr; true = 무한 반복
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 12px 0 0 0; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; UI 셋업이 완료된 후에도 타이머가 멈추지 않는다. 이벤트/델리게이트 기반으로 교체 필요.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 버그 8 --&gt;
&lt;div style=&quot;border: 1px solid #feca57; border-radius: 8px; padding: 20px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;color: #f39c12; font-weight: bold; margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;  Medium 2 &amp;mdash; 맵 이름 하드코딩&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 12px 0; font-size: 14px; color: #666;&quot; data-ke-size=&quot;size16&quot;&gt;위치: &lt;code&gt;AFLobbyPlayerController.cpp&lt;/code&gt;&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 6px; padding: 16px; margin: 8px 0;&quot;&gt;
&lt;pre class=&quot;isbl&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;code&gt;// ❌ 맵 이름 문자열 직접 하드코딩
if (MapName == TEXT(&quot;AFOTeamSelect&quot;))
if (MapName == TEXT(&quot;AFOCharacterSelectMap&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 12px 0 0 0; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 맵 이름이 바뀌면 코드도 같이 수정해야 한다. DataAsset 또는 GameInstance 변수로 분리 필요.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 버그 9 --&gt;
&lt;div style=&quot;border: 1px solid #feca57; border-radius: 8px; padding: 20px; margin: 20px 0;&quot;&gt;
&lt;p style=&quot;color: #f39c12; font-weight: bold; margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;  Medium 3 &amp;mdash; ApplySpeedBuff / AddShield 타이머 미정리&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 12px 0; font-size: 14px; color: #666;&quot; data-ke-size=&quot;size16&quot;&gt;위치: 캐릭터 스킬 관련 코드&lt;/p&gt;
&lt;div style=&quot;background: #1e1e1e; border-radius: 6px; padding: 16px; margin: 8px 0;&quot;&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #d4d4d4; font-size: 13px; margin: 0;&quot;&gt;&lt;code&gt;// ❌ 기존 타이머 정리 없이 재호출
// ClearTimer 없이 SetTimer 재호출
GetWorldTimerManager().SetTimer(
    SpeedBuffTimerHandle, ...
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 12px 0 0 0; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 버프가 중첩 적용되거나 타이머가 꼬여서 버프 해제 타이밍이 부정확해진다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 버그 요약 --&gt;
&lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  버그 요약&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background: #1a1a2e; color: white;&quot;&gt;
&lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;등급&lt;/th&gt;
&lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;버그&lt;/th&gt;
&lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;영향&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff4757; font-weight: bold;&quot;&gt;  Critical&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;HandleSkillHitCheck 세미콜론&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;스킬 데미지 전혀 안 들어감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff4757; font-weight: bold;&quot;&gt;  Critical&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;AnimNotify_AttackHit HasAuthority 누락&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;멀티플레이 크래시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff4757; font-weight: bold;&quot;&gt;  Critical&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;AFAurora::StopSprint 오타&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;오로라 스프린트 속도 미복원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff4757; font-weight: bold;&quot;&gt;  Critical&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;DataTable nullptr 역참조&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;스킬 슬롯 미표시 / 크래시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff9f43; font-weight: bold;&quot;&gt;  High&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;매 프레임 Cast 체인&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;성능 + 확장성 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #ff9f43; font-weight: bold;&quot;&gt;  High&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;팀 비교 로직 반전&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;데미지 색상 반대로 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #f39c12; font-weight: bold;&quot;&gt;  Medium&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;UI 반복 타이머&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;불필요한 반복 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #f39c12; font-weight: bold;&quot;&gt;  Medium&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;맵 이름 하드코딩&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;유지보수성 저하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6; color: #f39c12; font-weight: bold;&quot;&gt;  Medium&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;타이머 미정리 패턴&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;버프 타이밍 부정확&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- 리팩토링 방향 --&gt;
&lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt; ️ 리팩토링 방향 및 로드맵&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background: #1a1a2e; color: white;&quot;&gt;
&lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;Phase&lt;/th&gt;
&lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;작업&lt;/th&gt;
&lt;th style=&quot;padding: 12px 16px; text-align: left; border: 1px solid #dee2e6;&quot;&gt;핵심 키워드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;#1&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;Critical 버그 수정 (세미콜론, HasAuthority, 오타)&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;서버 권한, 스킬 판정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;#2&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;Cast 체인 &amp;rarr; Interface 교체&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;의존성 제거, 확장성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;#3&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;GAS 도입 &amp;mdash; ASC + AttributeSet&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;GAS, 전투 시스템&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;#4&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;HP/Mana &amp;rarr; GameplayEffect 교체&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;GAS, 리플리케이션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;#5&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;스킬 &amp;rarr; GameplayAbility 교체&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;GAS, 스킬 시스템&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;#6&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;DataTable 로드 타이밍 안정화&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;PossessedBy, nullptr 가드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;#7&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;UI 타이머 &amp;rarr; 이벤트 기반 교체&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;Delegate, 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f8f9fa;&quot;&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;#8&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;드로우콜 측정 + 최적화&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;프로파일링, Before/After&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;#9&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;패키징 + 멀티플레이 크래시 최종 수정&lt;/td&gt;
&lt;td style=&quot;padding: 10px 16px; border: 1px solid #dee2e6;&quot;&gt;Dedicated Server, 네트워크&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- CLAUDE.md --&gt;
&lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  CLAUDE.md 세팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code가 이 프로젝트의 맥락을 이해하고 일관된 방향으로 작업할 수 있도록 &lt;b&gt;CLAUDE.md&lt;/b&gt;를 루트에 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IronBird 때와 달리 이번 CLAUDE.md의 핵심은 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;padding-left: 20px;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;알려진 버그 목록&lt;/b&gt; &amp;mdash; 파일 위치와 원인까지 명시해서 Claude Code가 맥락 없이 엉뚱한 수정을 하지 않도록&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GAS 도입 계획&lt;/b&gt; &amp;mdash; 교체 대상 클래스와 추가할 모듈을 미리 정의해서 작업 방향을 고정&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 배운 것 --&gt;
&lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  이번 작업에서 배운 것&lt;/h2&gt;
&lt;div style=&quot;background: #f0f4ff; border-radius: 8px; padding: 20px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px 0;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 분석만으로도 포트폴리오 소재가 나온다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #555;&quot; data-ke-size=&quot;size16&quot;&gt;새 기능을 추가하지 않아도 기존 코드에서 &lt;code&gt;if (bHit) return;&lt;/code&gt; 같은 치명적인 버그를 발견하고, 원인을 정확히 짚어내는 것 자체가 실무 능력의 증명이다. 특히 팀 프로젝트 코드를 분석해서 &quot;이 버그가 왜 생겼는지&quot;까지 설명할 수 있으면, 면접관 입장에서 &lt;b&gt;&quot;이 사람은 남의 코드를 읽을 줄 안다&quot;&lt;/b&gt;는 인상을 준다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 다음 포스팅 --&gt;
&lt;h2 style=&quot;font-size: 20px; color: #1a1a2e; border-left: 4px solid #e94560; padding-left: 12px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  다음 포스팅&lt;/h2&gt;
&lt;div style=&quot;background: #1a1a2e; border-radius: 8px; padding: 20px; color: white;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[AFO_Refactor #1]&lt;/b&gt; Critical 버그 수정 &amp;mdash; 세미콜론 하나가 스킬 시스템을 망가뜨린 이야기&lt;/p&gt;
&lt;p style=&quot;margin: 8px 0 0 0; font-size: 13px; color: #a8b2d8;&quot; data-ke-size=&quot;size16&quot;&gt;HandleSkillHitCheck 수정 + AnimNotify HasAuthority 누락 수정 + 서버 권한 처리 전면 점검까지.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>개인프로젝트/AFO_Refactor</category>
      <category>GAS</category>
      <category>ue5</category>
      <category>UnrealEngine</category>
      <category>게임회사취업</category>
      <category>네트워크</category>
      <category>리팩토링</category>
      <category>멀티플레이게임</category>
      <category>언리얼엔진</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/217</guid>
      <comments>https://moonsj50.tistory.com/217#entry217comment</comments>
      <pubDate>Tue, 9 Jun 2026 20:12:54 +0900</pubDate>
    </item>
    <item>
      <title>C++ STL unordered_map 완전 정리 &amp;mdash; 코딩테스트 실전편</title>
      <link>https://moonsj50.tistory.com/216</link>
      <description>&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; max-width: 720px; line-height: 1.8; color: #1f2328;&quot;&gt;&lt;!-- 헤더 --&gt;
&lt;div style=&quot;border-left: 4px solid #0969da; padding-left: 16px; margin-bottom: 32px;&quot;&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0 0 4px;&quot; data-ke-size=&quot;size16&quot;&gt;코딩테스트 준비 #1&lt;/p&gt;
&lt;h1 style=&quot;font-size: 24px; font-weight: bold; margin: 0 0 4px; color: #1f2328;&quot;&gt;C++ unordered_map 완전 정리 &amp;mdash; 해시맵 기초부터 실전까지&lt;/h1&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;STL &amp;middot; unordered_map &amp;middot; 해시맵 &amp;middot; 프로그래머스 &amp;middot; 완주하지 못한 선수&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 목표 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 32px;&quot; data-ke-size=&quot;size26&quot;&gt;  목표&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;코딩테스트 해시맵 유형을 C++로 풀기 위해 &lt;b&gt;unordered_map&lt;/b&gt; 기본 문법을 정리하고, 프로그래머스 &lt;b&gt;완주하지 못한 선수(Level 1)&lt;/b&gt; 문제에 직접 적용한다.&lt;/p&gt;
&lt;!-- map vs unordered_map --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  map vs unordered_map&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;map&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;unordered_map&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;내부 구조&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;레드블랙트리&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;해시테이블&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;탐색 시간복잡도&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #cf222e; border-bottom: 1px solid #d0d7de;&quot;&gt;O(log N)&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #1a7f37; border-bottom: 1px solid #d0d7de;&quot;&gt;평균 O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;키 정렬&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;자동 정렬됨&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;순서 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;코테 기본 선택&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #cf222e;&quot;&gt;❌&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1a7f37;&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background: #ddf4ff; border-left: 4px solid #0969da; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #0550ae; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;코딩테스트에서는 거의 항상 &lt;b&gt;unordered_map&lt;/b&gt;을 써라. 키 정렬이 필요한 경우에만 map을 쓴다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 기본 문법 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  기본 문법 &amp;mdash; 핵심 4가지&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;1. 추가 / 증가&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// 없으면 0 생성 후 +1, 있으면 +1&lt;/span&gt;&lt;br /&gt;counter[&lt;span style=&quot;color: #cf222e;&quot;&gt;&quot;kim&quot;&lt;/span&gt;]++;&lt;br /&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// 직접 대입 (있으면 덮어씀)&lt;/span&gt;&lt;br /&gt;counter[&lt;span style=&quot;color: #cf222e;&quot;&gt;&quot;kim&quot;&lt;/span&gt;] = 5;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;2. 존재 여부 확인 vs 값 접근&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// count() &amp;rarr; 키 존재 여부만 반환 (0 또는 1)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;if&lt;/span&gt; (counter.count(&lt;span style=&quot;color: #cf222e;&quot;&gt;&quot;kim&quot;&lt;/span&gt;) &amp;gt; 0) { }&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// [] 연산자 &amp;rarr; 값(int) 반환&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;int&lt;/span&gt; val = counter[&lt;span style=&quot;color: #cf222e;&quot;&gt;&quot;kim&quot;&lt;/span&gt;];&lt;/div&gt;
&lt;p style=&quot;margin: 8px 0 0; font-size: 13px; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;⚠️ count()는 값을 반환하는 게 아니다. 키가 있냐 없냐만 알려준다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;3. 삭제&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;counter.erase(&lt;span style=&quot;color: #cf222e;&quot;&gt;&quot;kim&quot;&lt;/span&gt;); &lt;span style=&quot;color: #1a7f37;&quot;&gt;// 키:값 쌍 자체가 사라짐&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;4. 순회&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// map의 원소 자체가 pair&amp;lt;string, int&amp;gt;다&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;for&lt;/span&gt; (&lt;span style=&quot;color: #0969da;&quot;&gt;auto&lt;/span&gt;&amp;amp; [key, value] : counter) &lt;span style=&quot;color: #1a7f37;&quot;&gt;// C++17 구조적 바인딩&lt;/span&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// key = 키(string), value = 값(int)&lt;/span&gt;&lt;br /&gt;}&lt;/div&gt;
&lt;/div&gt;
&lt;!-- insert vs [] --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;⚡ insert vs [ ] 연산자 차이&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;방법&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;키가 이미 있을 때&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;코테 사용 빈도&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;insert()&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;무시됨 (기존 값 유지)&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da;&quot;&gt;[] 연산자&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;덮어씀&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1a7f37;&quot;&gt;높음 ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- 실전 적용 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  실전 적용 &amp;mdash; 완주하지 못한 선수&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;마라톤 전체 참가자와 완주자 명단이 주어질 때, 완주하지 못한 선수 1명을 찾아라. 동명이인 있음.&lt;/p&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;완전탐색 &amp;rarr; O(N&amp;sup2;) &amp;rarr; 시간초과&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// find()로 하나씩 검색 &amp;rarr; O(N) &amp;times; N번 = O(N&amp;sup2;)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// N=100,000이면 100억 번 연산 &amp;rarr; 시간초과&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #1a7f37;&quot; data-ke-size=&quot;size16&quot;&gt;unordered_map &amp;rarr; O(N) &amp;rarr; 통과&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 13px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 10px 12px; margin: 8px 0; line-height: 1.8;&quot;&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;#include&lt;/span&gt; &amp;lt;string&amp;gt;&lt;br /&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;#include&lt;/span&gt; &amp;lt;vector&amp;gt;&lt;br /&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;#include&lt;/span&gt; &amp;lt;unordered_map&amp;gt;&lt;br /&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;using namespace&lt;/span&gt; std;&lt;br /&gt;&lt;br /&gt;string solution(vector&amp;lt;string&amp;gt; participant, vector&amp;lt;string&amp;gt; completion)&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;string answer = &lt;span style=&quot;color: #cf222e;&quot;&gt;&quot;&quot;&lt;/span&gt;;&lt;br /&gt;&amp;nbsp;&amp;nbsp;unordered_map&amp;lt;string, &lt;span style=&quot;color: #0969da;&quot;&gt;int&lt;/span&gt;&amp;gt; counter;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// 1. 전체 참가자 횟수 저장&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;for&lt;/span&gt; (&lt;span style=&quot;color: #0969da;&quot;&gt;auto&lt;/span&gt;&amp;amp; part : participant)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;counter[part]++;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// 2. 완주자 횟수 차감&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;for&lt;/span&gt; (&lt;span style=&quot;color: #0969da;&quot;&gt;auto&lt;/span&gt;&amp;amp; comp : completion)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;counter[comp]--;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// 3. 횟수가 남은 사람 = 완주 못한 선수&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;for&lt;/span&gt; (&lt;span style=&quot;color: #0969da;&quot;&gt;auto&lt;/span&gt;&amp;amp; [key, value] : counter)&lt;br /&gt;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;if&lt;/span&gt; (value &amp;gt; 0)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;answer = key;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;break&lt;/span&gt;;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #0969da;&quot;&gt;return&lt;/span&gt; answer;&lt;br /&gt;}&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #dafbe1; border-left: 4px solid #1a7f37; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #116329; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동명이인 처리가 자동으로 되는 이유:&lt;/b&gt; 이름이 아닌 &lt;b&gt;횟수&lt;/b&gt;로 추적하기 때문이다. &quot;kim&quot;이 참가자에 2명, 완주자에 1명 있으면 counter[&quot;kim&quot;] = 1로 남는다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 배운 것 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  배운 것&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;값 추가/증가&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;counter[key]++&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;값 감소&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;counter[key]--&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;존재 여부&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;counter.count(key) &amp;gt; 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;값 접근&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;counter[key]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;순회&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;for (auto&amp;amp; [key, value] : counter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;삭제&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da;&quot;&gt;counter.erase(key)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background: #fff8c5; border-left: 4px solid #d4a72c; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7d4e00; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음 포스팅:&lt;/b&gt; unordered_set으로 중복 제거하기 &amp;mdash; 프로그래머스 폰켓몬&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>코딩테스트 준비</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/216</guid>
      <comments>https://moonsj50.tistory.com/216#entry216comment</comments>
      <pubDate>Tue, 9 Jun 2026 16:19:41 +0900</pubDate>
    </item>
    <item>
      <title>[IronBird #9] Android 빌드 + 실기기 프로파일링 (Galaxy S23+)</title>
      <link>https://moonsj50.tistory.com/215</link>
      <description>&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; max-width: 720px; line-height: 1.8; color: #1f2328;&quot;&gt;&lt;!-- 헤더 --&gt;
&lt;div style=&quot;border-left: 4px solid #0969da; padding-left: 16px; margin-bottom: 32px;&quot;&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0 0 4px;&quot; data-ke-size=&quot;size16&quot;&gt;IronBird 개발일지 #9&lt;/p&gt;
&lt;h1 style=&quot;font-size: 24px; font-weight: bold; margin: 0 0 4px; color: #1f2328;&quot;&gt;Android 빌드 + 실기기 프로파일링 (Galaxy S23+)&lt;/h1&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Android NDK &amp;middot; APK 패키징 &amp;middot; 실기기 수치 측정 &amp;middot; PC vs 모바일 비교&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 목표 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 32px;&quot; data-ke-size=&quot;size26&quot;&gt;  목표&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;PC에서 개발한 IronBird를 Android 기기에서 실행하고, 실기기에서 프로파일링 수치를 측정한다. PC 수치와 비교해 모바일 최적화 효과를 검증한다.&lt;/p&gt;
&lt;div style=&quot;background: #ddf4ff; border-left: 4px solid #0969da; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #0550ae; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 기기: Galaxy S23+ (SM-S916N) &amp;middot; Android 빌드 타겟: arm64-v8a &amp;middot; 텍스처 포맷: ETC2&lt;/p&gt;
&lt;/div&gt;
&lt;!-- Android 빌드 설정 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  Android 빌드 설정&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; overflow: hidden; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; width: 40%; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;설정값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;Android NDK&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;r27c (UE5.7 요구사항)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;텍스처 포맷&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;ETC2 (범용 Android)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;화면 방향&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;Portrait (세로 고정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;border-bottom: 1px solid #d0d7de;&quot;&gt;텍스처 스트리밍 풀&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da;border-bottom: 1px solid #d0d7de;&quot;&gt;r.Streaming.PoolSize=0 (자동)&lt;/td&gt;
&lt;/tr&gt;
  &lt;td style=&quot;padding: 8px 14px; color: #1f2328; &quot;&gt;렌더링 API&lt;/td&gt;
  &lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; &quot;&gt;Android Multi (OpenGL ES3.2 + Vulkan)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- 트러블슈팅 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  트러블슈팅&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 1. NDK 버전 불일치로 빌드 실패&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; Android Studio가 설치한 NDK r30이 UE5.7 허용 범위(r27c~r28c)를 초과했다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; SetupAndroid.bat 실행으로 r27c 설치 후 Project Settings에서 NDK 경로를 r27c로 직접 지정.&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 12px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 8px; margin: 8px 0; line-height: 1.8;&quot;&gt;C:\Program Files\Epic Games\UE_5.7\Engine\Extras\Android\SetupAndroid.bat&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 2. Platform Android is not a valid platform to build&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; UE5 엔진에 Android 플랫폼 지원 파일이 설치되지 않았다. NDK와 별개로 Epic Games Launcher에서 추가 설치가 필요하다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; Epic Games Launcher &amp;rarr; UE 5.7 옵션 &amp;rarr; 대상 플랫폼 &amp;rarr; Android 체크 &amp;rarr; 설치 (약 9GB).&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 3. 모바일에서 게임오버 터치 입력 안됨&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; PC에서는 GetMousePosition으로 좌표를 가져왔지만 모바일 터치는 다른 입력 경로를 사용한다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; BindTouch로 터치 좌표를 받아서 버튼 Rect와 비교하는 방식으로 변경.&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 12px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 8px; margin: 8px 0; line-height: 1.8;&quot;&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// 터치 입력 처리 추가&lt;/span&gt;&lt;br /&gt;void OnTouchPressed(ETouchIndex::Type FingerIndex, FVector Location)&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;float TouchX = Location.X;&lt;br /&gt;&amp;nbsp;&amp;nbsp;float TouchY = Location.Y;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #656d76;&quot;&gt;// 버튼 Rect와 비교&lt;/span&gt;&lt;br /&gt;}&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 4. 첫 실행 시 간헐적 크래시&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; 첫 실행 시 기기에서 셰이더를 컴파일하면서 메모리 spike 발생. 두 번째 실행부터는 캐시된 셰이더를 사용해 정상 작동.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론:&lt;/b&gt; 데모 시연 시 사전에 한 번 실행해두면 문제없음.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 실기기 수치 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  실기기 프로파일링 결과&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;측정 방법: Development 빌드 콘솔(stat fps / stat unit)로 실기기에서 직접 측정&lt;/p&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHU9S6/dJMcaiDA9Dq/HcV6laAkefiI9rGooKxuC1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHU9S6/dJMcaiDA9Dq/HcV6laAkefiI9rGooKxuC1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1404&quot; data-filename=&quot;KakaoTalk_20260607_234642092_01.jpg&quot; style=&quot;width: 49.3056%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHU9S6/dJMcaiDA9Dq/HcV6laAkefiI9rGooKxuC1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHU9S6%2FdJMcaiDA9Dq%2FHcV6laAkefiI9rGooKxuC1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;1404&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHCYTP/dJMcajibwb5/LbOhqZDhmUJkPTmTTIBckK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHCYTP/dJMcajibwb5/LbOhqZDhmUJkPTmTTIBckK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;1404&quot; data-filename=&quot;KakaoTalk_20260607_234642092.jpg&quot; style=&quot;width: 49.3056%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHCYTP/dJMcajibwb5/LbOhqZDhmUJkPTmTTIBckK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHCYTP%2FdJMcajibwb5%2FLbOhqZDhmUJkPTmTTIBckK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;1404&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;

&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; overflow: hidden; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;PC (Before)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;모바일 S23+&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;FPS&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;56.91&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #1a7f37; border-bottom: 1px solid #d0d7de;&quot;&gt;59.40 ✅&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;30fps 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;Game ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;18.01ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #1a7f37; border-bottom: 1px solid #d0d7de;&quot;&gt;3.79ms ✅&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;Draw ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;0.00ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #1a7f37; border-bottom: 1px solid #d0d7de;&quot;&gt;3.92ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;GPU Time&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;3.34ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #1a7f37; border-bottom: 1px solid #d0d7de;&quot;&gt;7.84ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;Draws&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #cf222e; border-bottom: 1px solid #d0d7de;&quot;&gt;252&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #1a7f37; border-bottom: 1px solid #d0d7de;&quot;&gt;34 ✅&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;100 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;메모리&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #cf222e;&quot;&gt;4.70GB&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #1a7f37;&quot;&gt;490MB ✅&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76;&quot;&gt;1.5GB 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background: #dafbe1; border-left: 4px solid #1a7f37; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #116329; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 결과:&lt;/b&gt; PC에서 Draws 252였던 것이 모바일에서 34로 측정됐다. PC stat 수치가 모바일 실제 렌더링 구조를 반영하지 못한다는 것을 확인했다. 모바일 최적화는 반드시 실기기에서 검증해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 배운 것 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  배운 것&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; Android 빌드는 NDK 버전과 엔진 플랫폼 파일 두 가지가 모두 맞아야 한다. 하나만 설치해서는 빌드가 안 된다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; PC stat 수치와 모바일 실측 수치는 완전히 다르다. PC Draws 252가 모바일에서 34로 나왔다. PC에서 최적화 효과가 미미해도 모바일에서 효과가 클 수 있다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; 터치 입력은 마우스 입력과 경로가 다르다. GetMousePosition 대신 BindTouch로 별도 처리해야 한다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; 첫 실행 셰이더 컴파일은 모바일 개발의 공통 이슈다. 데모 시연 전 사전 실행으로 캐시를 만들어두는 것이 필요하다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; Android Multi 빌드로 OpenGL ES3.2와 Vulkan을 동시에 지원할 수 있다. 기기가 자동으로 지원하는 렌더러를 선택한다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- IronBird 시리즈 마무리 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  IronBird 시리즈 마무리&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 13px; border: 1px solid #d0d7de; overflow: hidden; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;포스팅&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;핵심 내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;#0~1&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;프로젝트 셋업, 플레이어 이동 + 터치 입력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;#2~3&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;배경 스크롤, Object Pool (Game ms -25%, FPS +27%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;#4~5&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;적 스폰 시스템, 보스 3페이즈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;#6~7&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;메인화면 + 점수 시스템, BGM + 이펙트 + 완성도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;#8~9&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;최적화 시도 + Android 빌드 + 실기기 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;background: #fff8c5; border-left: 4px solid #d4a72c; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7d4e00; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가 예정:&lt;/b&gt; Google Play Store 출시 진행 중. 심사 완료 후 링크 업데이트 예정.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>개인프로젝트/IronBird</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/215</guid>
      <comments>https://moonsj50.tistory.com/215#entry215comment</comments>
      <pubDate>Mon, 8 Jun 2026 00:16:39 +0900</pubDate>
    </item>
    <item>
      <title>[IronBird #8] 드로우콜 최적화 시도 - PC 프로파일링 결과 분석</title>
      <link>https://moonsj50.tistory.com/214</link>
      <description>&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; max-width: 720px; line-height: 1.8; color: #1f2328;&quot;&gt;&lt;!-- 헤더 --&gt;
&lt;div style=&quot;border-left: 4px solid #0969da; padding-left: 16px; margin-bottom: 32px;&quot;&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0 0 4px;&quot; data-ke-size=&quot;size16&quot;&gt;IronBird 개발일지 #8&lt;/p&gt;
&lt;h1 style=&quot;font-size: 24px; font-weight: bold; margin: 0 0 4px; color: #1f2328;&quot;&gt;드로우콜 최적화 시도 - PC 프로파일링 결과 분석&lt;/h1&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;Shadow Casting &amp;middot; Object Pool &amp;middot; 콜리전 분리 &amp;middot; 프로파일링&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 목표 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 32px;&quot; data-ke-size=&quot;size26&quot;&gt;  목표&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;모바일 목표 수치(드로우콜 100 이하, FPS 30 안정)를 향해 최적화를 시도한다. PC에서 프로파일링하고 각 최적화 기법의 실제 효과를 측정한다.&lt;/p&gt;
&lt;div style=&quot;background: #ddf4ff; border-left: 4px solid #0969da; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #0550ae; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;최적화는 반드시 수치로 증명해야 한다. &quot;빠를 것 같다&quot;는 감이 아니라 Before/After 측정이 기준이다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- Before 수치 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  Before 수치&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;측정 환경: PC, 엔진 퀄리티 중간, 게임플레이 중 적 다수 존재&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; overflow: hidden; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;수치&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;모바일 목표&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;FPS&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;56.91&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;30fps 안정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;Frame&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;18.04ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;33ms 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;Game&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #cf222e; border-bottom: 1px solid #d0d7de;&quot;&gt;18.01ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;GPU Time&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;3.34ms&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;Draws&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #cf222e; border-bottom: 1px solid #d0d7de;&quot;&gt;252&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;100 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;메모리&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #cf222e;&quot;&gt;4.70GB&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76;&quot;&gt;1~1.5GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- 최적화 시도 --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;995&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckTxRF/dJMcaiQ5Xy5/NgqV6q0QoFiDDADoyO99w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckTxRF/dJMcaiQ5Xy5/NgqV6q0QoFiDDADoyO99w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckTxRF/dJMcaiQ5Xy5/NgqV6q0QoFiDDADoyO99w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckTxRF%2FdJMcaiQ5Xy5%2FNgqV6q0QoFiDDADoyO99w1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;995&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;995&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  최적화 시도 결과&lt;/h2&gt;
&lt;!-- Object Pool --&gt;
&lt;div style=&quot;background: #dafbe1; border-left: 4px solid #1a7f37; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px; font-size: 14px; font-weight: bold; color: #116329;&quot; data-ke-size=&quot;size16&quot;&gt;✅ Object Pool - 효과 있음 (이전 #3에서 측정)&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #116329;&quot; data-ke-size=&quot;size16&quot;&gt;Game 스레드 17.35ms &amp;rarr; 13.05ms (-25%), FPS 59 &amp;rarr; 76 (+27%)&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 콜리전 분리 --&gt;
&lt;div style=&quot;background: #dafbe1; border-left: 4px solid #1a7f37; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px; font-size: 14px; font-weight: bold; color: #116329;&quot; data-ke-size=&quot;size16&quot;&gt;✅ 콜리전 아키텍처 분리 - 구조적 개선&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #116329;&quot; data-ke-size=&quot;size16&quot;&gt;MeshComponent NoCollision + USphereComponent hitbox 분리. FBX 복잡한 콜리전 계산 제거.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 라이팅 제거 --&gt;
&lt;div style=&quot;background: #dafbe1; border-left: 4px solid #1a7f37; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px; font-size: 14px; font-weight: bold; color: #116329;&quot; data-ke-size=&quot;size16&quot;&gt;✅ 불필요한 라이팅 제거&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #116329;&quot; data-ke-size=&quot;size16&quot;&gt;DirectionalLight, SkyAtmosphere, VolumetricCloud, ExponentialHeightFog 제거. Unlit 머티리얼 사용으로 라이팅 연산 불필요.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- Shadow Casting Off --&gt;
&lt;div style=&quot;background: #fff8c5; border-left: 4px solid #d4a72c; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px; font-size: 14px; font-weight: bold; color: #7d4e00;&quot; data-ke-size=&quot;size16&quot;&gt;⚠️ Shadow Casting Off - PC에서 효과 미미&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #7d4e00;&quot; data-ke-size=&quot;size16&quot;&gt;총알&amp;middot;적&amp;middot;플레이어 MeshComponent-&amp;gt;SetCastShadow(false) 적용. Draws 252 &amp;rarr; 302로 오히려 증가. 이미 Unlit 머티리얼이라 Shadow 연산이 없었던 것이 원인.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- Pool 축소 --&gt;
&lt;div style=&quot;background: #dafbe1; border-left: 4px solid #1a7f37; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 4px; font-size: 14px; font-weight: bold; color: #116329;&quot; data-ke-size=&quot;size16&quot;&gt;✅ Object Pool 크기 축소&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #116329;&quot; data-ke-size=&quot;size16&quot;&gt;BulletPool 30&amp;rarr;20개, EnemyBulletPool 15개로 축소. 초기 메모리 할당 감소.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 분석 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  분석&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;PC에서 Draws 252는 게임 오브젝트보다 씬 자체(SM_SkySphere, SkyLight 등)의 비중이 크다. 게임 오브젝트 최적화만으로는 전체 드로우콜을 100 이하로 낮추기 어렵다.&lt;/p&gt;
&lt;div style=&quot;background: #ffebe9; border-left: 4px solid #cf222e; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #82071e; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 인사이트:&lt;/b&gt; PC stat 수치는 참고용이다. 모바일 최적화의 실제 지표는 Android 실기기에서 Unreal Insights로 측정해야 한다. PC에서 최적화 효과가 미미했던 Shadow Casting Off도 모바일 GPU에서는 다른 결과가 나올 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 배운 것 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  배운 것&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; 최적화는 항상 수치로 검증해야 한다. &quot;효과가 있을 것 같다&quot;는 감으로 적용하면 오히려 역효과가 날 수 있다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; Shadow Casting Off는 Lit 머티리얼에서 효과적이다. Unlit 머티리얼에서는 이미 Shadow 연산이 없어 효과가 없다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; PC 드로우콜과 모바일 드로우콜은 다르다. 실기기 측정 없이 PC 수치만으로 최적화 완료를 판단할 수 없다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; Object Pool은 드로우콜보다 Game 스레드 ms에 영향을 준다. 최적화 목표에 맞는 지표를 선택해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 다음 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  다음 작업&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[IronBird #9]&lt;/b&gt; Android 빌드 + 실기기 프로파일링&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #656d76;&quot; data-ke-size=&quot;size16&quot;&gt;Android SDK/NDK 세팅 후 실기기 빌드. Unreal Insights로 실측 수치 기록. PC와 모바일 수치 비교.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>개인프로젝트/IronBird</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/214</guid>
      <comments>https://moonsj50.tistory.com/214#entry214comment</comments>
      <pubDate>Fri, 5 Jun 2026 16:54:42 +0900</pubDate>
    </item>
    <item>
      <title>[IronBird #7] BGM + 이펙트 + 하트 아이템 + 틸팅 + UMG 메인화면</title>
      <link>https://moonsj50.tistory.com/213</link>
      <description>&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; max-width: 720px; line-height: 1.8; color: #1f2328;&quot;&gt;&lt;!-- 헤더 --&gt;
&lt;div style=&quot;border-left: 4px solid #0969da; padding-left: 16px; margin-bottom: 32px;&quot;&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0 0 4px;&quot; data-ke-size=&quot;size16&quot;&gt;IronBird 개발일지 #7&lt;/p&gt;
&lt;h1 style=&quot;font-size: 24px; font-weight: bold; margin: 0 0 4px; color: #1f2328;&quot;&gt;BGM + 이펙트 + 하트 아이템 + 틸팅 + UMG 메인화면&lt;/h1&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;UAudioComponent &amp;middot; SpawnEmitter &amp;middot; UMG &amp;middot; 카메라 고정 &amp;middot; 난이도 조정&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 목표 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 32px;&quot; data-ke-size=&quot;size26&quot;&gt;  목표&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;게임 완성도를 높인다. BGM과 효과음으로 타격감을 살리고, 하트 아이템과 난이도 조정으로 게임성을 추가한다. 카메라를 레벨에 고정해 갤러그 스타일 시점을 완성하고, UMG로 메인화면을 교체한다.&lt;/p&gt;
&lt;div style=&quot;background: #ddf4ff; border-left: 4px solid #0969da; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #0550ae; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;이번 작업의 핵심은 C++ 코드로 UPROPERTY 슬롯을 노출하고, 에디터에서 에셋을 할당하는 방식으로 분리한 것이다. 코드 수정 없이 에셋만 교체할 수 있어 유지보수가 편해진다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 구현 내용 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  구현 내용&lt;/h2&gt;
&lt;/div&gt;
&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; max-width: 720px; line-height: 1.8; color: #1f2328;&quot;&gt;
&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://youtube.com/shorts/lW6pr6lhA-8?feature=share&quot;&gt;https://youtube.com/shorts/lW6pr6lhA-8?feature=share&lt;/a&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://youtube.com/shorts/lW6pr6lhA-8?feature=share&quot;&gt; &amp;nbsp;&lt;/a&gt;&lt;span style=&quot;font-size: 16px; letter-spacing: 0px;&quot;&gt;IronBird 시연영상&lt;/span&gt;&lt;/h3&gt;
&lt;/div&gt;
&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; max-width: 720px; line-height: 1.8; color: #1f2328;&quot;&gt;점수 기반 하트 스폰타이머 방식 &amp;rarr; 1000점마다 하트 아이템 스폰으로 변경BGM 루프 재생OnAudioFinished 델리게이트로 BGM 반복 구현
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; overflow: hidden; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; width: 35%; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;BGM 시스템&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;Gameplay/Boss/GameOver/GameClear 4종, 상황별 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;효과음&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;피격음, 발사음(볼륨 30%), 폭발음, 아이템 획득음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;폭발 이펙트&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;적/보스 처치 시 SpawnEmitterAtLocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;하트 아이템&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;20~30초 랜덤 스폰, 획득 시 HP+1 (최대 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;난이도 조정&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;0~30초 3초, 30~60초 2초, 60~90초 1.5초 스폰 간격&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;TypeB 편대 스폰&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;0.3초 간격 3대 연속 스폰 (갤러그 스타일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;카메라 고정&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;레벨 CameraActor 고정, SpringArm 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;플레이어 틸팅&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;Y축 이동 속도 기반 Roll &amp;plusmn;25도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;UMG 메인화면&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;WBP_MainMenu, DrawText 방식 &amp;rarr; UMG 교체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;나이아가라 전환&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;ParticleSystem &amp;rarr; NiagaraSystem 교체, Niagara 모듈 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;타입별 점수 조정&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;TypeA 150점 / TypeB 50점 / TypeC 100점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da;&quot;&gt;보스 피격음&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;Boss HitSound UPROPERTY 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJfpQ0/dJMcaaFzlwe/4Lvgw4MYpfzn3ajKqnD9v1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJfpQ0/dJMcaaFzlwe/4Lvgw4MYpfzn3ajKqnD9v1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJfpQ0/dJMcaaFzlwe/4Lvgw4MYpfzn3ajKqnD9v1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJfpQ0%2FdJMcaaFzlwe%2F4Lvgw4MYpfzn3ajKqnD9v1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;802&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;1. BGM 시스템 설계&lt;/h3&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;UAudioComponent로 루프 재생하고 상황에 따라 전환하는 방식이다. UPROPERTY로 슬롯을 노출해 코드 수정 없이 에셋만 교체할 수 있다.&lt;/p&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0; font-family: monospace; font-size: 13px; color: #1f2328; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// BGM 전환 흐름&lt;/span&gt;&lt;br /&gt;BeginPlay &amp;rarr; GameplayBGM 재생&lt;br /&gt;SpawnBoss() &amp;rarr; BossBGM 전환 (null이면 유지)&lt;br /&gt;게임오버 &amp;rarr; GameOverBGM 전환&lt;br /&gt;보스 처치 &amp;rarr; GameClearBGM 전환&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// 발사음은 볼륨 30%로 BGM과 균형&lt;/span&gt;&lt;br /&gt;UGameplayStatics::PlaySoundAtLocation(&lt;br /&gt;&amp;nbsp;&amp;nbsp;this, ShootSound, GetActorLocation(), &lt;span style=&quot;color: #0969da;&quot;&gt;0.3f&lt;/span&gt;);&lt;/div&gt;
&lt;div style=&quot;background: #fff8c5; border-left: 4px solid #d4a72c; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7d4e00; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계 결정:&lt;/b&gt; 적 발사음은 의도적으로 제외했다. 적이 많아질수록 발사음이 겹쳐 BGM과 피격음이 묻히기 때문이다. 갤러그, 1945도 동일한 방식이다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 카메라 고정 --&gt;
&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;2. 카메라 고정 (갤러그 스타일)&lt;/h3&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;기존 SpringArm + Camera 구조는 플레이어를 따라다녀서 화면이 같이 이동하는 문제가 있었다. 레벨에 CameraActor를 고정 배치하고 PlayerController에서 뷰를 전환하는 방식으로 해결했다.&lt;/p&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0; font-family: monospace; font-size: 13px; color: #1f2328; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #cf222e;&quot;&gt;// Before: SpringArm이 플레이어 따라다님&lt;/span&gt;&lt;br /&gt;SpringArm-&amp;gt;TargetArmLength = 2000.0f;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// After: 레벨 고정 카메라로 뷰 전환&lt;/span&gt;&lt;br /&gt;ACameraActor* FixedCam = Cast&amp;lt;ACameraActor&amp;gt;(&lt;br /&gt;&amp;nbsp;&amp;nbsp;UGameplayStatics::GetActorOfClass(...));&lt;br /&gt;SetViewTargetWithBlend(FixedCam, 0.0f);&lt;/div&gt;
&lt;!-- 틸팅 --&gt;
&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;3. 플레이어 틸팅&lt;/h3&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;스켈레탈 메시 애니메이션 없이 코드로 구현했다. Y축 이동 방향에 따라 메시의 Roll 값을 조정해 기체가 자연스럽게 기우는 효과를 낸다.&lt;/p&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0; font-family: monospace; font-size: 13px; color: #1f2328; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// Tick에서 목표 위치와 현재 위치 차이로 기울기 계산&lt;/span&gt;&lt;br /&gt;float TiltAngle = FMath::Clamp(&lt;br /&gt;&amp;nbsp;&amp;nbsp;(TargetLocation.Y - GetActorLocation().Y) * -0.05f,&lt;br /&gt;&amp;nbsp;&amp;nbsp;-25.0f, 25.0f);&lt;br /&gt;&lt;br /&gt;MeshComponent-&amp;gt;SetRelativeRotation(&lt;br /&gt;&amp;nbsp;&amp;nbsp;FRotator(0.0f, -90.0f, 90.0f + TiltAngle));&lt;/div&gt;
&lt;div style=&quot;background: #fff8c5; border-left: 4px solid #d4a72c; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7d4e00; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모바일 관점:&lt;/b&gt; StarSparrow는 스태틱 메시라 스켈레탈 애니메이션이 불가능하다. 코드 기반 틸팅은 별도 애니메이션 에셋 없이 구현 가능하고 모바일 성능 부담도 없다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 추가 구현 --&gt;
&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;4. 나이아가라 전환&lt;/h3&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;기존 Cascade 파티클 시스템(UParticleSystem)을 나이아가라(UNiagaraSystem)로 교체했다. UE5에서 Cascade는 레거시이며 나이아가라가 공식 파티클 시스템이다.&lt;/p&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0; font-family: monospace; font-size: 13px; color: #1f2328; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #cf222e;&quot;&gt;// Before: Cascade&lt;/span&gt;&lt;br /&gt;UGameplayStatics::SpawnEmitterAtLocation(&lt;br /&gt;&amp;nbsp;&amp;nbsp;GetWorld(), ExplosionEffect, GetActorLocation());&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// After: Niagara&lt;/span&gt;&lt;br /&gt;UNiagaraFunctionLibrary::SpawnSystemAtLocation(&lt;br /&gt;&amp;nbsp;&amp;nbsp;GetWorld(), ExplosionEffect, GetActorLocation());&lt;/div&gt;
&lt;div style=&quot;background: #fff8c5; border-left: 4px solid #d4a72c; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7d4e00; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Build.cs 주의:&lt;/b&gt; Niagara 사용 시 IronBird.Build.cs에 &quot;Niagara&quot; 모듈을 반드시 추가해야 한다. 누락 시 링크 에러 발생.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;5. 점수 기반 하트 스폰&lt;/h3&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;기존 타이머 기반 하트 스폰은 게임 진행과 무관하게 일정 시간마다 나왔다. 점수 기반으로 변경해 게임 흐름과 연동했다.&lt;/p&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0; font-family: monospace; font-size: 13px; color: #1f2328; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #cf222e;&quot;&gt;// Before: 20~30초 랜덤 타이머&lt;/span&gt;&lt;br /&gt;SetTimer(HeartTimerHandle, ..., FMath::RandRange(20.f, 30.f));&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;// After: 1000점마다 스폰&lt;/span&gt;&lt;br /&gt;if (GS-&amp;gt;GetScore() &amp;gt;= NextHeartScoreThreshold)&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;SpawnHeartItem();&lt;br /&gt;&amp;nbsp;&amp;nbsp;NextHeartScoreThreshold += 1000;&lt;br /&gt;}&lt;/div&gt;
&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;6. BGM 루프 재생&lt;/h3&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;UAudioComponent에 직접 루프 설정이 불안정해 OnAudioFinished 델리게이트로 반복 재생을 구현했다.&lt;/p&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0; font-family: monospace; font-size: 13px; color: #1f2328; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// BGM 재생 시 OnAudioFinished 바인딩&lt;/span&gt;&lt;br /&gt;BGMComponent-&amp;gt;OnAudioFinished.AddDynamic(&lt;br /&gt;&amp;nbsp;&amp;nbsp;this, &amp;amp;AIronBirdGameMode::RestartBGM);&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// RestartBGM: 처음부터 다시 재생&lt;/span&gt;&lt;br /&gt;void AIronBirdGameMode::RestartBGM()&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;BGMComponent-&amp;gt;Play(0.0f);&lt;br /&gt;}&lt;/div&gt;
&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;7. 타입별 점수 조정&lt;/h3&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; overflow: hidden; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;타입&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;변경 전&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;변경 후&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;TypeA&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;100점&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #1a7f37; border-bottom: 1px solid #d0d7de;&quot;&gt;150점&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;빠른 직선탄, 위협도 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;TypeB&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;100점&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #cf222e; border-bottom: 1px solid #d0d7de;&quot;&gt;50점&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;3대 연속 등장, 개당 점수 낮게&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;TypeC&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #656d76;&quot;&gt;100점&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; color: #656d76;&quot;&gt;100점&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76;&quot;&gt;유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- 트러블슈팅 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  트러블슈팅&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 1. 메인메뉴 &amp;rarr; 게임 진입 후 마우스 이동 불가&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; 메인메뉴에서 설정한 FInputModeGameAndUI가 레벨 전환 후에도 유지됐다. Slate가 마우스 입력을 가로채 PlayerTick의 IsInputKeyDown이 false를 반환했다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; AIronBirdPlayerController BeginPlay에서 FInputModeGameOnly로 명시적 초기화.&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 12px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 8px; margin: 8px 0; line-height: 1.8;&quot;&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;FInputModeGameOnly GameInputMode;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;SetInputMode(GameInputMode);&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 2. WBP_MainMenu MCP 자동 생성 실패&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; UE5.7 Python API에서 Widget Blueprint의 WidgetTree와 이벤트 그래프 편집이 차단돼 있다. MCP로 레이아웃 자동 구성이 불가능했다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; C++ 코드로 UMG 위젯을 뷰포트에 추가하는 부분만 자동화하고, 위젯 내부 디자인은 에디터에서 직접 구성했다.&lt;/p&gt;
&lt;div style=&quot;background: #ddf4ff; border-left: 3px solid #0969da; padding: 8px 12px; border-radius: 0 4px 4px 0; margin: 8px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #0550ae; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;AI 도구의 한계를 파악하고 수동 작업으로 전환한 것도 포트폴리오 소재가 된다. &quot;AI가 할 수 있는 것과 없는 것을 판단할 수 있는 사람&quot;이 목표이기 때문이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- 배운 것 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  배운 것&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; UPROPERTY로 사운드/이펙트 슬롯을 노출하면 코드 수정 없이 에디터에서 에셋만 교체할 수 있다. 기획 변경에 유연한 구조다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; FInputMode는 레벨 전환 후에도 유지된다. 레벨 시작 시 항상 명시적으로 초기화해야 한다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; 스태틱 메시도 코드로 Roll 값을 조정하면 애니메이션 느낌을 낼 수 있다. 모바일에서 애니메이션 에셋 없이 구현하는 좋은 대안이다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; 사운드 설계는 &quot;무엇을 넣느냐&quot;보다 &quot;무엇을 뺄 것인가&quot;가 중요하다. 적 발사음을 제외한 것이 전체 사운드 균형을 유지하는 데 핵심이었다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; UE5.7 Python MCP API는 Widget Blueprint 내부 편집을 지원하지 않는다. AI 도구의 한계를 파악하고 빠르게 수동 작업으로 전환하는 판단이 중요하다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 다음 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  다음 작업&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[IronBird #8]&lt;/b&gt; 드로우콜 최적화 (Before/After 핵심)&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #656d76;&quot; data-ke-size=&quot;size16&quot;&gt;머티리얼 인스턴싱, 스태틱 메시 병합으로 드로우콜을 줄이고 수치를 기록한다. 이 시리즈에서 가장 임팩트 있는 포스팅이 될 예정이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>개인프로젝트/IronBird</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/213</guid>
      <comments>https://moonsj50.tistory.com/213#entry213comment</comments>
      <pubDate>Tue, 2 Jun 2026 14:24:28 +0900</pubDate>
    </item>
    <item>
      <title>[Coding Test] 시뮬레이션 격자(Grid) 탐색 핵심 개념 정리 (dy, dx, Offset)</title>
      <link>https://moonsj50.tistory.com/212</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 격자판 좌표가 (y, x)인 이유: 2차원 배열의 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 수학 좌표계에서는 가로축이 X, 세로축이 Y이므로 (x, y)로 표기하는 것이 익숙합니다. 하지만 코딩테스트의 격자판 문제는 컴퓨터 메모리의 &lt;b&gt;2차원 배열&lt;/b&gt;을 기반으로 작동합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;2차원 배열 구조:&lt;/b&gt; &lt;code&gt;Array[Row][Column]&lt;/code&gt; -&amp;gt; &lt;code&gt;[행][열]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;매핑 방식:&lt;/b&gt; 행(Row)은 &lt;b&gt;위아래(가로줄)&lt;/b&gt;를 결정하므로 Y축이 되고, 열(Column)은 &lt;b&gt;좌우(세로줄)&lt;/b&gt;를 결정하므로 X축이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 코딩테스트 시뮬레이션 문제를 풀 때는 항상 &lt;b&gt;(y, x) 즉 [행][열] 구조&lt;/b&gt;를 표준으로 삼고 접근해야 메모리 접근 효율(Cache Locality)을 높이고 버그를 줄일 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 윗칸으로 이동하는데 y - 1인 이유: Screen Coordinate&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수학 그래프에서는 위로 올라갈수록 Y값이 증가하지만, 컴퓨터 그래픽스와 2차원 배열에서는 &lt;b&gt;좌측 상단(Top-Left)이 기준점 (0, 0)&lt;/b&gt;이 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;오른쪽으로 이동:&lt;/b&gt; X축 증가 (&lt;code&gt;x + 1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아래쪽으로 이동:&lt;/b&gt; Y축 증가 (&lt;code&gt;y + 1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위쪽으로 이동:&lt;/b&gt; Y축 감소 (&lt;code&gt;y - 1&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트를 위에서 아래로 읽어 내려가는 인간의 시선 및 메모리 주소 할당 방향과 일치하기 때문입니다. 기준점 (y, x)를 중심으로 상하좌우를 표현하면 아래 구조와 같습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;(y - 1, x) [상]&lt;/th&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;(y, x - 1) [좌]&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;(y, x) [기준점]&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;(y, x + 1) [우]&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;(y + 1, x) [하]&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 방향 벡터 dy, dx에서 d의 의미&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시뮬레이션 문제 해설을 보면 항상 &lt;code&gt;int dy[4]&lt;/code&gt;, &lt;code&gt;int dx[4]&lt;/code&gt; 같은 배열이 등장합니다. 여기서 &lt;b&gt;d는 변화량, 차이를 뜻하는 델타(Delta) 또는 변위(Displacement)&lt;/b&gt;의 약자입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;현재 위치에서 다음 위치로 이동할 때 &lt;b&gt;좌표가 얼마나 변화해야 하는가?&lt;/b&gt;&quot;를 의미하는 상대적인 값(방향 벡터)을 상수로 묶어둔 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C++ 4방향 탐색 표준 템플릿 코드&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

// 상, 하, 좌, 우 순서로 변위(Delta) 정의
const int dy[4] = { -1, 1, 0, 0 };
const int dx[4] = { 0, 0, -1, 1 };

int main() {
    int currentY = 5, currentX = 5; // 현재 위치

    for (int i = 0; i &amp;lt; 4; ++i) {
        int nextY = currentY + dy[i];
        int nextX = currentX + dx[i];
        
        // 이 한 줄의 반복문으로 상하좌우 4방향 이동을 모두 처리할 수 있습니다.
        std::cout &amp;lt;&amp;lt; &quot;방향 [&quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &quot;] 이동 결과: (&quot; &amp;lt;&amp;lt; nextY &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; nextX &amp;lt;&amp;lt; &quot;)\\n&quot;;
    }
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이점:&lt;/b&gt; 수많은 &lt;code&gt;if-else&lt;/code&gt; 분기문을 제거하여 코드 가독성을 높이고, CPU의 분기 예측 성공률을 높여 연산 오버헤드를 최적화할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 오프셋(Offset)이란 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오프셋(Offset)&lt;/b&gt;은 &lt;b&gt;&quot;기준점(Origin)으로부터 얼마나 떨어져 있는가?&quot;&lt;/b&gt;를 나타내는 상대적인 거리나 변위를 뜻합니다. 절대적인 위치 좌표가 아니라, &lt;b&gt;'상대적인 차이값'&lt;/b&gt;이라는 점이 핵심입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 메모리 관점에서의 오프셋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 &lt;code&gt;A[2]&lt;/code&gt;에 접근할 때, 시스템 내부적으로는 배열의 시작 주소(Base Address)에 &lt;code&gt;[자료형 크기 * 인덱스]&lt;/code&gt;만큼의 &lt;b&gt;오프셋&lt;/b&gt;을 더해 물리적 주소를 찾아갑니다. 이 연산이 단 한 번에 끝나기 때문에 배열의 임의 접근(Random Access) 시간 복잡도는 &lt;b&gt;O(1)&lt;/b&gt;이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 게임 엔진 및 시뮬레이션 관점에서의 오프셋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몬스터가 플레이어를 추적하거나 캐릭터 전방에 타격 판정을 생성할 때, 캐릭터의 현재 좌표를 &lt;b&gt;기준점(Base)&lt;/b&gt;으로 잡고, 바라보는 방향 벡터에 거리를 곱한 &lt;b&gt;오프셋(Offset)&lt;/b&gt;을 더해 최종 목적지 좌표를 계산합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #f8fafc; padding: 10px; border-radius: 4px; text-align: center; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;최종 목적지 위치 = 기준 위치 + 오프셋&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 요약 및 한계 인식 (Technical Insight)&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;[y][x] 구조&lt;/b&gt;와 &lt;b&gt;좌상단 (0, 0) 기준의 축 방향&lt;/b&gt;은 메모리 구조 및 화면 렌더링 방식에 기인한 프로그래밍의 표준 규칙입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dy&lt;/code&gt;, &lt;code&gt;dx&lt;/code&gt;는 복잡한 분기문 없이 주변을 탐색하기 위한 &lt;b&gt;변위 오프셋 상수&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한계 인식:&lt;/b&gt; 2D 격자 시뮬레이션의 (y, x) 좌표계를 실제 3D 게임 엔진(예: Unreal Engine 5)의 월드 좌표계(X 전방, Y 우측 오른손 법칙)로 이식할 때는, 축 매핑 과정에서 축이 뒤바뀌거나 부동소수점 오차로 인한 정밀도 손실이 발생할 수 있으므로 명확한 축 변환 연산과 예외 처리가 필요합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p class=&quot;tag&quot; data-ke-size=&quot;size16&quot;&gt;#코딩테스트 #C++ #시뮬레이션 #방향벡터 #오프셋 #게임개발자 #기본기다지기&lt;/p&gt;</description>
      <category>코딩테스트 준비</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/212</guid>
      <comments>https://moonsj50.tistory.com/212#entry212comment</comments>
      <pubDate>Mon, 1 Jun 2026 18:28:05 +0900</pubDate>
    </item>
    <item>
      <title>[IronBird #6] 메인화면 + 점수 시스템 + 게임오버/클리어</title>
      <link>https://moonsj50.tistory.com/211</link>
      <description>&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; max-width: 720px; line-height: 1.8; color: #1f2328;&quot;&gt;&lt;!-- 헤더 --&gt;
&lt;div style=&quot;border-left: 4px solid #0969da; padding-left: 16px; margin-bottom: 32px;&quot;&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0 0 4px;&quot; data-ke-size=&quot;size16&quot;&gt;IronBird 개발일지 #6&lt;/p&gt;
&lt;h1 style=&quot;font-size: 24px; font-weight: bold; margin: 0 0 4px; color: #1f2328;&quot;&gt;메인화면 + 점수 시스템 + 게임오버/클리어&lt;/h1&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;MainMenuHUD &amp;middot; GameState &amp;middot; DrawText &amp;middot; FInputModeGameAndUI&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 목표 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 32px;&quot; data-ke-size=&quot;size26&quot;&gt;  목표&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;게임의 시작부터 끝까지 순환 구조를 완성한다. 메인화면 &amp;rarr; 게임플레이 &amp;rarr; 게임오버/클리어 &amp;rarr; 메인화면으로 돌아오는 흐름과 점수 시스템을 구현한다.&lt;/p&gt;
&lt;div style=&quot;background: #ddf4ff; border-left: 4px solid #0969da; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #0550ae; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;메인화면 &amp;rarr; GAME START &amp;rarr; 게임플레이 &amp;rarr; GAME OVER/CLEAR &amp;rarr; MAIN MENU &amp;rarr; 메인화면&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 구현 내용 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  구현 내용&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; overflow: hidden; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; width: 40%; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;클래스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;AMainMenuHUD&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;IRONBIRD 타이틀, GAME START / QUIT GAME 버튼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;AMainMenuGameMode&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;메인화면 전용 GameMode, 마우스 입력 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;AIronBirdGameState&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;점수 관리 (적 100점, 보스 1000점)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;AIronBirdHUD 수정&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;SCORE 우측상단, GAME OVER/CLEAR 오버레이, MAIN MENU 버튼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da;&quot;&gt;배경&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;SM_SkySphere + Galaxy_Mat_Inst, Tick 회전으로 스크롤 효과&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- 점수 시스템 --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVcAk6/dJMcaipXpIE/Q27Wy4LGzkGlsybhBvOovK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVcAk6/dJMcaipXpIE/Q27Wy4LGzkGlsybhBvOovK/img.png&quot; data-alt=&quot;Main Menu&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVcAk6/dJMcaipXpIE/Q27Wy4LGzkGlsybhBvOovK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVcAk6%2FdJMcaipXpIE%2FQ27Wy4LGzkGlsybhBvOovK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;293&quot; height=&quot;373&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Main Menu&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;#6.gif&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEkisF/dJMcajh4LSX/TaOi8XwsBLbjO3i1IepJE1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEkisF/dJMcajh4LSX/TaOi8XwsBLbjO3i1IepJE1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEkisF/dJMcajh4LSX/TaOi8XwsBLbjO3i1IepJE1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bEkisF/dJMcajh4LSX/TaOi8XwsBLbjO3i1IepJE1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;433&quot; height=&quot;658&quot; data-filename=&quot;#6.gif&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;점수 시스템 구조&lt;/h3&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0; font-family: monospace; font-size: 13px; color: #1f2328; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// Bullet Tick &amp;rarr; Enemy 감지 &amp;rarr; 점수 추가&lt;/span&gt;&lt;br /&gt;Enemy-&amp;gt;bKilledByPlayer = &lt;span style=&quot;color: #0969da;&quot;&gt;true&lt;/span&gt;;&lt;br /&gt;Enemy-&amp;gt;ReturnToPool();&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// Enemy ReturnToPool()에서&lt;/span&gt;&lt;br /&gt;if (bKilledByPlayer)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;GS-&amp;gt;AddScore(&lt;span style=&quot;color: #0969da;&quot;&gt;100&lt;/span&gt;);&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// Boss 처치 시&lt;/span&gt;&lt;br /&gt;GS-&amp;gt;AddScore(&lt;span style=&quot;color: #0969da;&quot;&gt;1000&lt;/span&gt;);&lt;/div&gt;
&lt;!-- 트러블슈팅 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  트러블슈팅&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 1. 메인화면 버튼 클릭이 동작하지 않음&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; BeginPlay에서 &lt;code style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; padding: 1px 4px; border-radius: 3px; font-size: 12px;&quot;&gt;FInputModeUIOnly&lt;/code&gt;를 설정하면 모든 마우스 입력이 Slate UI로만 전달된다. 이 모드에서는 &lt;code style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; padding: 1px 4px; border-radius: 3px; font-size: 12px;&quot;&gt;IsInputKeyDown&lt;/code&gt;이 항상 false를 반환한다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; &lt;code style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; padding: 1px 4px; border-radius: 3px; font-size: 12px;&quot;&gt;FInputModeGameAndUI&lt;/code&gt;로 변경. 게임 입력 시스템과 Slate를 동시에 활성화해 IsInputKeyDown이 정상 동작하게 했다.&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 12px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 8px; margin: 8px 0; line-height: 1.8;&quot;&gt;&lt;span style=&quot;color: #cf222e;&quot;&gt;- FInputModeUIOnly InputMode;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;+ FInputModeGameAndUI InputMode;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;+ InputMode.SetHideCursorDuringCapture(false);&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 2. 적 처치해도 점수가 오르지 않음&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; Bullet 메시가 NoCollision이라 Enemy::OnOverlapBegin이 절대 호출되지 않았다. bKilledByPlayer 플래그 세팅 없이 ReturnToPool만 호출되어 점수가 항상 0이었다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; Bullet Tick 수동 쿼리에서 Enemy-&amp;gt;ReturnToPool() 직전에 bKilledByPlayer = true 추가.&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 12px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 8px; margin: 8px 0; line-height: 1.8;&quot;&gt;&lt;span style=&quot;color: #1a7f37;&quot;&gt;+ Enemy-&amp;gt;bKilledByPlayer = true;&lt;/span&gt;&lt;br /&gt;Enemy-&amp;gt;ReturnToPool();&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 3. HDR Cubemap 텍스처를 배경 Plane에 적용 불가&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; milkywaysky_HDR는 Cubemap 형식이라 일반 TextureSample2D 노드와 타입 불일치. Plane 메시에 직접 적용 불가.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; Background_Plane 제거. 레벨의 SM_SkySphere에 Galaxy_Mat_Inst 적용 후 Tick에서 X축 회전으로 별이 흘러가는 스크롤 효과 구현.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 배운 것 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  배운 것&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; FInputModeUIOnly는 게임 입력을 차단한다. HUD에서 마우스 클릭을 감지하려면 FInputModeGameAndUI를 써야 한다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; 충돌 감지 흐름이 바뀌면 플래그 세팅 위치도 같이 바뀌어야 한다. 수동 쿼리 방식은 이벤트 방식과 호출 경로가 다르다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; HDR Cubemap은 Skybox 전용이다. 일반 메시에 적용하려면 별도 노드가 필요하다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; GameMode Tick은 기본 비활성이다. PrimaryActorTick.bCanEverTick = true를 생성자에서 명시해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 다음 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  다음 작업&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[IronBird #7]&lt;/b&gt; 난이도 조정 + 하트 아이템 + BGM + 이펙트 + 틸팅 + HUD 정리&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #656d76;&quot; data-ke-size=&quot;size16&quot;&gt;게임 완성도를 높이고 Android 빌드 준비.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>개인프로젝트/IronBird</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/211</guid>
      <comments>https://moonsj50.tistory.com/211#entry211comment</comments>
      <pubDate>Thu, 28 May 2026 22:19:00 +0900</pubDate>
    </item>
    <item>
      <title>[IronBird #5] 보스 페이즈 + 적 3종류 + 충돌 시스템 구현</title>
      <link>https://moonsj50.tistory.com/210</link>
      <description>&lt;div style=&quot;font-family: 'Noto Sans KR', sans-serif; max-width: 720px; line-height: 1.8; color: #1f2328;&quot;&gt;&lt;!-- 헤더 --&gt;
&lt;div style=&quot;border-left: 4px solid #0969da; padding-left: 16px; margin-bottom: 32px;&quot;&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0 0 4px;&quot; data-ke-size=&quot;size16&quot;&gt;IronBird 개발일지 #5&lt;/p&gt;
&lt;h1 style=&quot;font-size: 24px; font-weight: bold; margin: 0 0 4px; color: #1f2328;&quot;&gt;보스 페이즈 + 적 3종류 + 충돌 시스템 구현&lt;/h1&gt;
&lt;p style=&quot;color: #656d76; font-size: 13px; margin: 0;&quot; data-ke-size=&quot;size16&quot;&gt;EBossPhase &amp;middot; USphereComponent &amp;middot; FBX 콜리전 &amp;middot; 수동 물리 쿼리&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 목표 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 32px;&quot; data-ke-size=&quot;size26&quot;&gt;  목표&lt;/h2&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;3페이즈 패턴 전환 보스와 3종류 적을 구현한다. 플레이어 체력 시스템과 HUD도 추가한다. FBX 메시 교체 과정에서 예상치 못한 콜리전 문제가 발생해 충돌 시스템을 전면 재설계했다.&lt;/p&gt;
&lt;div style=&quot;background: #ddf4ff; border-left: 4px solid #0969da; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #0550ae; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;AI로 클래스 구조 생성 &amp;rarr; FBX 콜리전 문제 발견 &amp;rarr; 직접 원인 분석 &amp;rarr; USphereComponent로 재설계&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 구현 내용 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  구현 내용&lt;/h2&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; font-size: 14px; border: 1px solid #d0d7de; overflow: hidden; margin: 12px 0;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; width: 40%; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #656d76; border-bottom: 1px solid #d0d7de;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;적 3종류&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;TypeA 직선+발사 / TypeB 지그재그 / TypeC 느린하강+부채꼴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;ABoss&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;HP 50, EBossPhase 3단계, HomeX 사인 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;플레이어 체력&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;MaxHP=3, 피격 2초 무적, TakeDamage 오버라이드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da; border-bottom: 1px solid #d0d7de;&quot;&gt;HUD&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328; border-bottom: 1px solid #d0d7de;&quot;&gt;DrawText 하트 3개 + 보스 체력바 (UMG 없이)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 8px 14px; font-family: monospace; font-size: 13px; color: #0969da;&quot;&gt;메시 교체&lt;/td&gt;
&lt;td style=&quot;padding: 8px 14px; color: #1f2328;&quot;&gt;StarSparrow FBX (플레이어/적/보스 색상 구분)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;!-- 보스 페이즈 --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;#5.gif&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EN0jg/dJMcac4jXw4/FzmETGODoCUU4dt5FbUFDK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EN0jg/dJMcac4jXw4/FzmETGODoCUU4dt5FbUFDK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EN0jg/dJMcac4jXw4/FzmETGODoCUU4dt5FbUFDK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/EN0jg/dJMcac4jXw4/FzmETGODoCUU4dt5FbUFDK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;433&quot; height=&quot;658&quot; data-filename=&quot;#5.gif&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxZFjE/dJMcaipXgqi/7k5ZKtxAwA0LGnfq6QbiqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxZFjE/dJMcaipXgqi/7k5ZKtxAwA0LGnfq6QbiqK/img.png&quot; data-alt=&quot;플레이어 비행기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxZFjE/dJMcaipXgqi/7k5ZKtxAwA0LGnfq6QbiqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxZFjE%2FdJMcaipXgqi%2F7k5ZKtxAwA0LGnfq6QbiqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;178&quot; height=&quot;167&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;플레이어 비행기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckFFBH/dJMcagr8kWT/m2tFQ7kM2eqsf5G7L8eW30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckFFBH/dJMcagr8kWT/m2tFQ7kM2eqsf5G7L8eW30/img.png&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;624&quot; data-is-animation=&quot;false&quot; width=&quot;197&quot; height=&quot;184&quot; style=&quot;width: 37.3863%; margin-right: 10px;&quot; data-widthpercent=&quot;38.45&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckFFBH/dJMcagr8kWT/m2tFQ7kM2eqsf5G7L8eW30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckFFBH%2FdJMcagr8kWT%2Fm2tFQ7kM2eqsf5G7L8eW30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAIixO/dJMcahExKrW/uLszgmnp28sUJTHbP862xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAIixO/dJMcahExKrW/uLszgmnp28sUJTHbP862xK/img.png&quot; data-origin-width=&quot;268&quot; data-origin-height=&quot;315&quot; data-is-animation=&quot;false&quot; style=&quot;width: 29.6685%; margin-right: 10px;&quot; data-widthpercent=&quot;30.52&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAIixO/dJMcahExKrW/uLszgmnp28sUJTHbP862xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAIixO%2FdJMcahExKrW%2FuLszgmnp28sUJTHbP862xK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;268&quot; height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2rqLH/dJMcahxPxuu/EjSTSHnPwx1fhCWefEL1Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2rqLH/dJMcahxPxuu/EjSTSHnPwx1fhCWefEL1Wk/img.png&quot; data-origin-width=&quot;295&quot; data-origin-height=&quot;341&quot; data-is-animation=&quot;false&quot; width=&quot;165&quot; height=&quot;191&quot; style=&quot;width: 30.1674%;&quot; data-widthpercent=&quot;31.03&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2rqLH/dJMcahxPxuu/EjSTSHnPwx1fhCWefEL1Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2rqLH%2FdJMcahxPxuu%2FEjSTSHnPwx1fhCWefEL1Wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;295&quot; height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;TypeA, TypeB, TypeC 적 비행기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;362&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zb9Co/dJMcai4uIsM/DjfKH4zsU4CQDkFb9RQgBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zb9Co/dJMcai4uIsM/DjfKH4zsU4CQDkFb9RQgBK/img.png&quot; data-alt=&quot;보스 비행기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zb9Co/dJMcai4uIsM/DjfKH4zsU4CQDkFb9RQgBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzb9Co%2FdJMcai4uIsM%2FDjfKH4zsU4CQDkFb9RQgBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;362&quot; height=&quot;256&quot; data-origin-width=&quot;362&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;보스 비행기&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 style=&quot;font-size: 15px; font-weight: bold; color: #1f2328; margin-top: 28px;&quot; data-ke-size=&quot;size23&quot;&gt;1. 보스 페이즈 상태머신&lt;/h3&gt;
&lt;p style=&quot;color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;Behavior Tree 없이 HP 비율 기반 단순 상태머신으로 구현했다. 모바일에서 BT는 오버엔지니어링이다.&lt;/p&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0; font-family: monospace; font-size: 13px; color: #1f2328; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// HP 비율로 페이즈 전환&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;Phase1&lt;/span&gt; (HP &amp;gt;60%): X 고정, Y 왕복 / 1.2초마다 직선탄 5발&lt;br /&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;Phase2&lt;/span&gt; (HP &amp;gt;30%): X 사인이동, Y 왕복 / 0.9초마다 부채꼴 5발&lt;br /&gt;&lt;span style=&quot;color: #0969da;&quot;&gt;Phase3&lt;/span&gt; (HP &amp;le;30%): 속도 1.5배 / 0.6초마다 전체 패턴&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// HomeX 기반 사인 이동 (스폰 위치 중심으로 진동)&lt;/span&gt;&lt;br /&gt;X = HomeX + FMath::Sin(Time) * 200.0f;&lt;/div&gt;
&lt;div style=&quot;background: #fff8c5; border-left: 4px solid #d4a72c; padding: 12px 16px; border-radius: 0 6px 6px 0; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7d4e00; font-size: 14px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모바일 관점:&lt;/b&gt; Behavior Tree는 틱마다 태스크를 평가해 모바일에서 부담이 크다. 보스처럼 패턴이 명확한 경우 HP 비율 기반 상태머신이 훨씬 가볍고 예측 가능하다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 트러블슈팅 핵심 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  트러블슈팅&lt;/h2&gt;
&lt;!-- 트러블슈팅 1 --&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 1. FBX 메시 교체 후 모든 충돌이 동작하지 않음 (핵심)&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; UE5에서 FBX 임포트 시 콜리전 Shape가 자동 생성되지 않는다. 기존 Cube 메시는 내장 BoxCollision이 있어서 동작했지만, StarSparrow FBX는 물리 바디가 없어 모든 충돌이 불가능했다.&lt;/p&gt;
&lt;p style=&quot;margin: 8px 0 4px; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; MeshComponent의 콜리전을 비활성화하고 USphereComponent를 별도로 추가해 hitbox를 명시적으로 구성했다.&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 12px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 12px; margin: 8px 0; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// MeshComponent는 비주얼 전용&lt;/span&gt;&lt;br /&gt;MeshComponent-&amp;gt;SetCollisionEnabled(ECollisionEnabled::NoCollision);&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// 명시적 hitbox 추가&lt;/span&gt;&lt;br /&gt;HitSphere = CreateDefaultSubobject&amp;lt;USphereComponent&amp;gt;(TEXT(&quot;HitSphere&quot;));&lt;br /&gt;HitSphere-&amp;gt;SetupAttachment(RootComponent);&lt;br /&gt;HitSphere-&amp;gt;SetSphereRadius(80.0f);&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #656d76;&quot;&gt;// Enemy: 80 / Boss: 250 / Player: 70&lt;/span&gt;&lt;br /&gt;HitSphere-&amp;gt;SetCollisionObjectType(ECC_Pawn);&lt;br /&gt;HitSphere-&amp;gt;SetGenerateOverlapEvents(true);&lt;/div&gt;
&lt;/div&gt;
&lt;!-- 트러블슈팅 2 --&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 2. QueryOnly 컴포넌트 간 Overlap 이벤트 불안정&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; UE5 Chaos 물리에서 QueryOnly 설정된 컴포넌트끼리는 OnComponentBeginOverlap 이벤트가 간헐적으로 발생하지 않는 경우가 있다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; Bullet Tick에서 OverlapMultiByObjectType으로 직접 물리 쿼리를 실행했다.&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 12px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 12px; margin: 8px 0; line-height: 2; overflow-x: auto;&quot;&gt;&lt;span style=&quot;color: #656d76;&quot;&gt;// 이벤트 방식 대신 직접 쿼리&lt;/span&gt;&lt;br /&gt;TArray&amp;lt;FOverlapResult&amp;gt; Overlaps;&lt;br /&gt;FCollisionObjectQueryParams ObjParams(ECC_Pawn);&lt;br /&gt;GetWorld()-&amp;gt;OverlapMultiByObjectType(&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Overlaps, GetActorLocation(), FQuat::Identity,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ObjParams, FCollisionShape::MakeSphere(50.0f), QueryParams&lt;br /&gt;);&lt;/div&gt;
&lt;div style=&quot;background: #fff8c5; border-left: 3px solid #d4a72c; padding: 8px 12px; border-radius: 0 4px 4px 0; margin: 8px 0;&quot;&gt;
&lt;p style=&quot;margin: 0; color: #7d4e00; font-size: 13px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모바일 관점:&lt;/b&gt; Bullet Tick마다 물리 쿼리를 실행하므로 총알 수가 많을수록 비용이 증가한다. 현재 Object Pool로 최대 30개로 제한돼 있어 허용 범위지만, 탄막 슈팅이라면 LineTrace 기반이 더 효율적이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;!-- 트러블슈팅 3 --&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 8px; font-size: 14px; font-weight: bold; color: #cf222e;&quot; data-ke-size=&quot;size16&quot;&gt;문제 3. FBX 메시 방향이 탑뷰 기준과 불일치&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인:&lt;/b&gt; FBX 모델의 전면 방향이 UE5 좌표계 기준과 달라 탑뷰에서 옆을 향하게 됐다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결:&lt;/b&gt; 에디터에서 직접 확인하며 SetRelativeRotation 값을 맞췄다.&lt;/p&gt;
&lt;div style=&quot;font-family: monospace; font-size: 12px; color: #1f2328; background: #ffffff; border: 1px solid #d0d7de; border-radius: 4px; padding: 12px; margin: 8px 0; line-height: 2;&quot;&gt;플레이어: FRotator(0, -90, 90)&lt;br /&gt;적/보스: FRotator(0, 90, 90)&lt;/div&gt;
&lt;/div&gt;
&lt;!-- 배운 것 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  배운 것&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; FBX 임포트 메시는 콜리전이 없다. 항상 USphereComponent 또는 UCapsuleComponent로 별도 hitbox를 구성해야 한다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; MeshComponent는 비주얼 전용, Collision 전용 컴포넌트를 분리하는 것이 명확한 설계다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; UE5 Chaos에서 QueryOnly 컴포넌트 간 Overlap 이벤트는 불안정할 수 있다. 중요한 충돌은 직접 물리 쿼리로 처리하는 것이 안정적이다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; 보스 AI는 BT 없이 HP 비율 기반 상태머신으로 충분하다. 모바일에서 BT는 불필요한 오버헤드다.&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;bull; FBX 좌표계는 엔진마다 다르다. 임포트 후 항상 에디터에서 방향을 직접 확인해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;!-- 다음 --&gt;
&lt;h2 style=&quot;font-size: 17px; font-weight: bold; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; margin-top: 40px;&quot; data-ke-size=&quot;size26&quot;&gt;  다음 작업&lt;/h2&gt;
&lt;div style=&quot;background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; margin: 12px 0;&quot;&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #1f2328;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[IronBird #6]&lt;/b&gt; 메인화면 + 게임오버 + 점수 시스템 + 회복아이템 + BGM + 이펙트 + 효과음&lt;/p&gt;
&lt;p style=&quot;margin: 4px 0; font-size: 14px; color: #656d76;&quot; data-ke-size=&quot;size16&quot;&gt;적 처치 시 100점, 게임오버/클리어 시 점수 표시, 메인화면 레벨 분리.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>개인프로젝트/IronBird</category>
      <author>Client Side</author>
      <guid isPermaLink="true">https://moonsj50.tistory.com/210</guid>
      <comments>https://moonsj50.tistory.com/210#entry210comment</comments>
      <pubDate>Thu, 28 May 2026 20:09:51 +0900</pubDate>
    </item>
  </channel>
</rss>