TIL
07.21 TIL
2026. 7. 21. 20:18

일정표

시간 할 일 비고
08:00~10:00 코드카타  
10:00~13:00 개인프로젝트  
14:00~18:00 언리얼 강의, 팀프로젝트  
19:00~21:00 팀프로젝트  
21:00~22:00 운동  
22:00~23:30 개인 프로젝트  

*오늘의 코드카타*

 

문제. 문자열 나누기

더보기

문자열 s가 입력되었을 때 다음 규칙을 따라서 이 문자열을 여러 문자열로 분해하려고 합니다.

먼저 첫 글자를 읽습니다. 이 글자를 x라고 합시다.
이제 이 문자열을 왼쪽에서 오른쪽으로 읽어나가면서, x와 x가 아닌 다른 글자들이 나온 횟수를 각각 셉니다. 처음으로 두 횟수가 같아지는 순간 멈추고, 지금까지 읽은 문자열을 분리합니다.
s에서 분리한 문자열을 빼고 남은 부분에 대해서 이 과정을 반복합니다. 남은 부분이 없다면 종료합니다.
만약 두 횟수가 다른 상태에서 더 이상 읽을 글자가 없다면, 역시 지금까지 읽은 문자열을 분리하고, 종료합니다.
문자열 s가 매개변수로 주어질 때, 위 과정과 같이 문자열들로 분해하고, 분해한 문자열의 개수를 return 하는 함수 solution을 완성하세요.

#include <string>

using namespace std;

int solution(string s) {
    int answer = 0;
    
    char x = ' ';
    int same_cnt = 0;
    int diff_cnt = 0;
    
    for (char c : s)
    {
        if (same_cnt == 0 && diff_cnt == 0)
        {
            x = c;
            same_cnt = 1;
            continue;
        }
        
        if (c == x)
        {
            same_cnt++;
        }
        else
        {
            diff_cnt++;
        }
        
        if (same_cnt == diff_cnt)
        {
            answer++;
            same_cnt = 0;
            diff_cnt = 0;
        }
    }
    if (same_cnt > 0 || diff_cnt > 0)
    {
        answer++;
    }
    return answer; 
}

*팀 프로젝트*

 

 

1. 작업한 내용들 (디버깅)

 

[1. 로비 레벨에서 E 상호작용 텍스트 출력안됨 ]

 

[2. 로비 레벨에서 던지기 차징게이지 출력 안됨 ]

 

[3. 옵션창 남아있는 상태에서 ESC 누르면옵션창남아있음 ]

 

유저가 옵션창이 떠 있는 상태에서 키보드 ESC를 누르는 단 1프레임 동안, 언리얼 엔진 내부에서는 두 개의 이벤트가 동시에 연속으로 실행되고 있었습니다.

 

1차 실행 (Slate UI 레이어):

UParcelOptionsWidget::NativeOnKeyDown이 ESC 키를 먼저 감지합니다.

 

HandleBackClicked() -> RemoveFromParent()가 실행되어 옵션창이 뷰포트에서 즉시 삭제됩니다.

 

2차 실행 (Enhanced Input 레이어 - IA_InGameMenu):

 

FInputModeGameAndUI 모드 특성상 키보드 ESC 입력이 캐릭터 컴포넌트의 UParcelHeroComponent::ToggleInGameMenu()로도 넘어갑니다.

ToggleInGameMenu() 내부에서 ESCMenuRef->CloseSubMenuIfOpen()을 호출합니다.

 

하지만 불과 직전에 1차 실행(OptionsWidget)에서 옵션창을 이미 RemoveFromParent 해버렸기 때문에, OptionsWidgetInstance->IsInViewport() 조건문이 false가 됩니다.

 

결국 CloseSubMenuIfOpen()은 "어? 서브메뉴 없는데?" 하고 false를 반환하고, ToggleInGameMenu()는 곧바로 ESC 메뉴(ESCMenuRef)까지 연달아 파괴해 버린 것입니다.

 

해결책 (단일 제어 창구 및 델리게이트 구조 적용)

 

ESC 키 제어의 일원화: 옵션창(UParcelOptionsWidget)은 ESC 키 입력 감지 시 스스로 삭제하지 않고 Enhanced Input(ToggleInGameMenu)이 서브메뉴 닫기를 전담하도록 통제권을 넘깁니다.

 

델리게이트(OnOptionsClosed) 도입: UI 내부의 Btn_Back(뒤로가기 버튼)을 마우스로 클릭했을 때는 델리게이트를 송출하여, 부모 위젯인 ESCMenuWidget이 안전하게 옵션창을 제거하고 포커스를 회수하도록 만듭니다.

 

 

옵션 창이 떠 있을 때 ESC 입력 시:

 

UParcelOptionsWidget::NativeOnKeyDown이 Unhandled를 반환하여 Slate 레이어에서 키를 가로채지 않고 넘깁니다.

 

UParcelHeroComponent::ToggleInGameMenu()가 실행됩니다.

 

CloseSubMenuIfOpen()이 호출되는 순간, 옵션창은 여전히 뷰포트에 잘 떠 있으므로(IsInViewport() == true), 옵션창만 깔끔히 제거되고 true를 반환하며 ESC 메뉴는 그대로 유지됩니다!

 

다시 한번 ESC 입력 시:

이제 서브메뉴가 없으므로 CloseSubMenuIfOpen()이 false를 반환하고, ESC 메뉴 본체가 정상적으로 닫힙니다.

 

마우스로 [뒤로가기] 버튼 클릭 시:

OnOptionsClosed 델리게이트가 터지면서 ESCMenuWidget::CloseOptionsWidget()이 호출되어 안전하게 닫히고 ESC 메뉴로 포커스가 되돌아옵니다.

 

 

[4. 손에든상자체력출력(InGameHUD, LobbyHUD추가) ]

 

[5. shop 버튼연동 ]

 

[6. 사운드 삽입]

 

[7. 맵 썸네일 제작]

 

[8. UI 상점, 아이템 사용 칸 연동]


*언리얼 강의*

1. 기초반 내용

 

언리얼 C++ 몬스터 근접 공격(AnimNotifyState 히트박스) 및 피격 처리

1. 개요 및 핵심 개념

몬스터가 플레이어를 추격한 뒤 근접 공격을 수행하고, 실제 플레이어에게 피해를 입히는 시스템을 구현합니다.

핵심 아이디어: AnimNotifyState 기반 히트박스 제어

  • 문제점: 무기(도끼)의 충돌 영역(Box Component)을 상시 활성화해두면, 몬스터가 이동하거나 단순 접근만 해도 플레이어가 피해를 입는 판정 오류가 발생합니다.
  • 해결책: 공격 애니메이션 몽타주의 특정 구간(도끼를 휘두르는 0.3~0.5초)에만 히트박스 충돌을 활성화합니다. 이를 관리하기 위해 UAnimNotifyState를 활용합니다.
  • 중복 피해 방지: 한 번의 공격 스윙 동안 동일한 대상이 중복해서 피해를 입지 않도록 TSet<AActor*> 구조를 사용하여 프레임별 중복 Overlap을 차단합니다.

2. 공용 타입 및 부모 클래스 확장

2-1. 독립 열거형 정의 (MeleeHitSide.h)

노티파이와 몬스터 클래스가 공용으로 사용할 열거형을 헤더 전용 파일로 분리하여 클래스 간 직접적인 의존성 결합을 방지합니다.

// Source/Public/Animation/MeleeHitSide.h
#pragma once

#include "CoreMinimal.h"
#include "MeleeHitSide.generated.h"

UENUM(BlueprintType)
enum class EMeleeHitSide : uint8
{
    Left  UMETA(DisplayName = "Left"),
    Right UMETA(DisplayName = "Right"),
    Both  UMETA(DisplayName = "Both"),
};

2-2. 부모 몬스터 클래스 확장 (AMonster)

사망 및 공격 가상 함수 인터페이스를 확장하고, 자식 클래스에서 사망 시점에 즉시 히트박스를 비활성화할 수 있도록 OnDeathBegin() 훅(Hook) 함수를 추가합니다.

// Monster.h
public:
    virtual void PlayAttackEffect();
    virtual void OnDeathBegin() {}

// Monster.cpp
void AMonster::PlayAttackEffect()
{
    // 기본 구현은 비워둠 (자식 클래스에서 오버라이드)
}

// Tick 내 사망 시퀀스 처리부
if (!bDeathSequenceStarted)
{
    bDeathSequenceStarted = true;
    OnDeathBegin(); // 자식 클래스 훅 호출: 사망 시 히트박스 강제 OFF
    // 캡슐 충돌 끄기, 이동 비활성화 등...
}

 

3. Khaimera 몬스터 클래스 구현 (AMonsterKhaimera)

스켈레탈 메시의 weapon_l, weapon_r 본(Bone)에 UBoxComponent를 부착하고, Overlap 이벤트를 처리합니다.

3-1. 헤더 파일 (MonsterKhaimera.h)

#pragma once

#include "CoreMinimal.h"
#include "Characters/Monsters/Monster.h"
#include "Animation/MeleeHitSide.h"
#include "MonsterKhaimera.generated.h"

UCLASS()
class TPSWAVESHOOTER_API AMonsterKhaimera : public AMonster
{
    GENERATED_BODY()

public:
    AMonsterKhaimera();

    virtual void BeginPlay() override;
    virtual void PlayAttackEffect() override;
    virtual void PlayDeathEffect() override;
    virtual void OnDeathBegin() override;

    void EnableMeleeHitbox(EMeleeHitSide HitSide);
    void DisableMeleeHitbox(EMeleeHitSide HitSide);

protected:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Melee")
    class UBoxComponent* LeftAxeHitbox;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Melee")
    class UBoxComponent* RightAxeHitbox;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Melee")
    int32 MeleeDamage = 30;

    UPROPERTY(EditDefaultsOnly, Category = "Sound")
    class USoundBase* AttackSound;

    UPROPERTY(EditDefaultsOnly, Category = "Sound")
    class USoundBase* DeathSound;

private:
    UFUNCTION()
    void OnAxeOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
                      UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
                      bool bFromSweep, const FHitResult& SweepResult);

    // 단일 스윙 내 중복 히트 방지용 집합
    UPROPERTY()
    TSet<AActor*> AlreadyHitThisSwing;
};

3-2. 소스 파일 (MonsterKhaimera.cpp)

#include "Characters/Monsters/MonsterKhaimera.h"
#include "Characters/Player/PlayerCharacter.h"
#include "Components/BoxComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Kismet/GameplayStatics.h"

namespace
{
    const FName LeftAxeBone  = TEXT("weapon_l");
    const FName RightAxeBone = TEXT("weapon_r");
}

AMonsterKhaimera::AMonsterKhaimera()
{
    GetMesh()->SetRelativeLocationAndRotation(FVector(0, 0, -88), FRotator(0, -90, 0));

    // 좌측 도끼 히트박스 설정 (기본 충돌 비활성화)
    LeftAxeHitbox = CreateDefaultSubobject<UBoxComponent>(TEXT("LeftAxeHitbox"));
    LeftAxeHitbox->SetupAttachment(GetMesh(), LeftAxeBone);
    LeftAxeHitbox->SetBoxExtent(FVector(50.0f, 15.0f, 15.0f));
    LeftAxeHitbox->SetRelativeLocation(FVector(50.0f, 0.0f, 0.0f));
    LeftAxeHitbox->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
    LeftAxeHitbox->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    LeftAxeHitbox->SetGenerateOverlapEvents(true);

    // 우측 도끼 히트박스 설정 (기본 충돌 비활성화)
    RightAxeHitbox = CreateDefaultSubobject<UBoxComponent>(TEXT("RightAxeHitbox"));
    RightAxeHitbox->SetupAttachment(GetMesh(), RightAxeBone);
    RightAxeHitbox->SetBoxExtent(FVector(50.0f, 15.0f, 15.0f));
    RightAxeHitbox->SetRelativeLocation(FVector(50.0f, 0.0f, 0.0f));
    RightAxeHitbox->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
    RightAxeHitbox->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    RightAxeHitbox->SetGenerateOverlapEvents(true);
}

void AMonsterKhaimera::BeginPlay()
{
    Super::BeginPlay();

    if (LeftAxeHitbox)
    {
        LeftAxeHitbox->OnComponentBeginOverlap.AddDynamic(this, &AMonsterKhaimera::OnAxeOverlap);
    }
    if (RightAxeHitbox)
    {
        RightAxeHitbox->OnComponentBeginOverlap.AddDynamic(this, &AMonsterKhaimera::OnAxeOverlap);
    }
}

void AMonsterKhaimera::OnDeathBegin()
{
    Super::OnDeathBegin();
    // 사망 시 노티파이 누락에 대비한 강제 비활성화
    if (LeftAxeHitbox)  { LeftAxeHitbox->SetCollisionEnabled(ECollisionEnabled::NoCollision); }
    if (RightAxeHitbox) { RightAxeHitbox->SetCollisionEnabled(ECollisionEnabled::NoCollision); }
}

void AMonsterKhaimera::PlayAttackEffect()
{
    if (AttackSound)
    {
        UGameplayStatics::PlaySoundAtLocation(GetWorld(), AttackSound, GetActorLocation(), 0.5f);
    }
}

void AMonsterKhaimera::PlayDeathEffect()
{
    Super::PlayDeathEffect();
    if (DeathSound)
    {
        UGameplayStatics::PlaySoundAtLocation(GetWorld(), DeathSound, GetActorLocation(), 0.5f);
    }
}

void AMonsterKhaimera::EnableMeleeHitbox(EMeleeHitSide HitSide)
{
    AlreadyHitThisSwing.Reset(); // 새 스윙 시작 시 중복 기록 초기화

    if (HitSide == EMeleeHitSide::Left || HitSide == EMeleeHitSide::Both)
    {
        if (LeftAxeHitbox) { LeftAxeHitbox->SetCollisionEnabled(ECollisionEnabled::QueryOnly); }
    }
    if (HitSide == EMeleeHitSide::Right || HitSide == EMeleeHitSide::Both)
    {
        if (RightAxeHitbox) { RightAxeHitbox->SetCollisionEnabled(ECollisionEnabled::QueryOnly); }
    }
}

void AMonsterKhaimera::DisableMeleeHitbox(EMeleeHitSide HitSide)
{
    if (HitSide == EMeleeHitSide::Left || HitSide == EMeleeHitSide::Both)
    {
        if (LeftAxeHitbox) { LeftAxeHitbox->SetCollisionEnabled(ECollisionEnabled::NoCollision); }
    }
    if (HitSide == EMeleeHitSide::Right || HitSide == EMeleeHitSide::Both)
    {
        if (RightAxeHitbox) { RightAxeHitbox->SetCollisionEnabled(ECollisionEnabled::NoCollision); }
    }
    AlreadyHitThisSwing.Reset();
}

void AMonsterKhaimera::OnAxeOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
                                    UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
                                    bool bFromSweep, const FHitResult& SweepResult)
{
    if (!OtherActor || OtherActor == this) { return; }

    APlayerCharacter* Player = Cast<APlayerCharacter>(OtherActor);
    if (!Player) { return; }

    // 단일 스윙 당 1회만 데미지 적용
    bool bAlreadyHit = false;
    AlreadyHitThisSwing.Add(Player, &bAlreadyHit);
    if (bAlreadyHit) { return; }

    Player->OnDamage(MeleeDamage);
}

4. AnimNotifyState를 통한 히트박스 제어

애니메이션 몽타주 구간 내에서 C++ 메소드를 호출할 UAnimNotifyState_MeleeHitbox를 정의합니다.

// AnimNotifyState_MeleeHitbox.h
#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "Animation/MeleeHitSide.h"
#include "AnimNotifyState_MeleeHitbox.generated.h"

UCLASS(meta = (DisplayName = "Melee Hitbox"))
class TPSWAVESHOOTER_API UAnimNotifyState_MeleeHitbox : public UAnimNotifyState
{
    GENERATED_BODY()

public:
    virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
                             float TotalDuration, const FAnimNotifyEventReference& EventReference) override;

    virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
                           const FAnimNotifyEventReference& EventReference) override;

protected:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Hitbox")
    EMeleeHitSide Side = EMeleeHitSide::Right;
};

// AnimNotifyState_MeleeHitbox.cpp
#include "Animation/AnimNotifyState_MeleeHitbox.h"
#include "Characters/Monsters/MonsterKhaimera.h"
#include "Components/SkeletalMeshComponent.h"

void UAnimNotifyState_MeleeHitbox::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
                                               float TotalDuration, const FAnimNotifyEventReference& EventReference)
{
    Super::NotifyBegin(MeshComp, Animation, TotalDuration, EventReference);
    if (!MeshComp) return;

    if (AMonsterKhaimera* Owner = Cast<AMonsterKhaimera>(MeshComp->GetOwner()))
    {
        Owner->EnableMeleeHitbox(Side);
    }
}

void UAnimNotifyState_MeleeHitbox::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
                                             const FAnimNotifyEventReference& EventReference)
{
    Super::NotifyEnd(MeshComp, Animation, EventReference);
    if (!MeshComp) return;

    if (AMonsterKhaimera* Owner = Cast<AMonsterKhaimera>(MeshComp->GetOwner()))
    {
        Owner->DisableMeleeHitbox(Side);
    }
}

 

5. AI 태스크, 서비스 및 상태 구동

5-1. 근접 공격 태스크 (UBTTask_MeleeAttack)

태스크는 애니메이션 몽타주 재생 신호만 찌르고 즉시 Succeeded를 반환합니다. 실제 데미지 판정은 AnimNotifyState가, 공격 간격 제어는 Behavior Tree의 Wait 노드가 담당합니다.

// BTTask_MeleeAttack.cpp
#include "AI/BTTask_MeleeAttack.h"
#include "AIController.h"
#include "Animation/MonsterAnim.h"
#include "Characters/Monsters/Monster.h"
#include "Components/SkeletalMeshComponent.h"

UBTTask_MeleeAttack::UBTTask_MeleeAttack()
{
    NodeName = TEXT("Melee Attack");
}

EBTNodeResult::Type UBTTask_MeleeAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    AAIController* AICon = OwnerComp.GetAIOwner();
    if (!AICon) return EBTNodeResult::Failed;

    AMonster* Monster = Cast<AMonster>(AICon->GetPawn());
    if (!Monster) return EBTNodeResult::Failed;

    if (UMonsterAnim* Anim = Cast<UMonsterAnim>(Monster->GetMesh()->GetAnimInstance()))
    {
        // Rising-edge 트리거를 확실히 잡기 위해 false -> true 시퀀스 적용
        Anim->bIsAttack = false;
        Anim->bIsAttack = true;
        Anim->PlayAttackMontage();
    }

    Monster->PlayAttackEffect();
    return EBTNodeResult::Succeeded;
}

 

5-2. AI 애니메이션 상태 제어 서비스 (UBTService_DriveAnimState)

이동 속도, 공격 여부, 피격 스턴, 사망 상태의 우선순위를 평가하여 AnimInstance의 AIState 변수를 매 0.1초마다 업데이트합니다.

// BTService_DriveAnimState.cpp
#include "AI/BTService_DriveAnimState.h"
#include "AIController.h"
#include "Animation/MonsterAnim.h"
#include "Characters/Monsters/Monster.h"
#include "Components/HealthComponent.h"
#include "Components/SkeletalMeshComponent.h"

UBTService_DriveAnimState::UBTService_DriveAnimState()
{
    NodeName = TEXT("Drive Anim State");
    Interval = 0.1f;
    RandomDeviation = 0.0f;
    bNotifyTick = true;
    bCallTickOnSearchStart = true;
}

void UBTService_DriveAnimState::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

    AAIController* AICon = OwnerComp.GetAIOwner();
    if (!AICon) return;

    AMonster* Monster = Cast<AMonster>(AICon->GetPawn());
    if (!Monster) return;

    UMonsterAnim* Anim = Cast<UMonsterAnim>(Monster->GetMesh()->GetAnimInstance());
    if (!Anim) return;

    // 우선순위 평가: 사망 -> 피격 -> 공격 -> 이동/대기

    // 1) 사망
    if (Monster->HealthComp && Monster->HealthComp->bIsDead) return;

    // 2) 피격 (Damage 상태 유지)
    if (Anim->AIState == EMonsterState::Damage) return;

    // 3) 공격
    if (Anim->bIsAttack)
    {
        Anim->AIState = EMonsterState::Attack;
        return;
    }

    // 4) 이동/대기 (속도 기준 판정)
    const float Speed = Monster->GetVelocity().Size();
    Anim->AIState = (Speed > MoveSpeedThreshold) ? EMonsterState::Move : EMonsterState::Idle;
}

 

5-3. MonsterAI 피격 및 사망 플래그 처리

피격 시 공격을 캔슬하고 스턴 타이머를 실행하며, 사망 시 블랙보드의 bIsDead 키를 true로 세팅하여 BT의 행동을 즉시 중단시킵니다.

void UMonsterAI::OnMonsterDamage(int32 DamageVal)
{
    if (!HealthComp || HealthComp->bIsDead) return;

    HealthComp->ApplyDamage(DamageVal);

    if (HealthComp->CurrentHP > 0)
    {
        if (Anim)
        {
            Anim->AIState = EMonsterState::Damage;
            Anim->bIsAttack = false; // 진행 중인 공격 인터럽트

            int32 Idx = FMath::RandRange(0, 1);
            Anim->PlayDamageMontage(FName(*FString::Printf(TEXT("HitReact%d"), Idx)));
        }

        if (!GetWorld()->GetTimerManager().IsTimerActive(StunResetTimer))
        {
            GetWorld()->GetTimerManager().SetTimer(
                StunResetTimer, this, &UMonsterAI::ResetStun, StunCoolTime, false);
        }
    }
    else
    {
        HealthComp->MarkDead();
        if (Anim)
        {
            Anim->bIsAttack = false;
            Anim->AIState = EMonsterState::Death;
            Anim->PlayDeathMontage(TEXT("DeathStart"));
        }

        if (Monster)
        {
            Monster->GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
            Monster->PlayDeathEffect();
        }

        // BT 추격/공격 중단 통지
        if (AIControl)
        {
            if (UBlackboardComponent* BB = AIControl->GetBlackboardComponent())
            {
                BB->SetValueAsBool(TEXT("bIsDead"), true);
            }
        }
    }
}

 

6. 애니메이션 블루프린트 및 슬롯 체인

6-1. Additive 피격 애니메이션 설정

피격 동작(HitReact_Front, HitReact_Back)이 이동 모션 위에서 자연스럽게 얹히도록 Additive 설정을 변경합니다.

  • Additive Anim Type: Local Space
  • Base Pose Type: Selected animation frame
  • Base Pose Animation: Idle_Zero_Pose

6-2. AnimGraph 슬롯 체인 구조

몽타주 간 상호 간섭을 방지하기 위해 슬롯을 기능별로 분리하여 체인 형태로 연결합니다.

[Locomotion State Machine] - [DefaultSlot] - [Slot 'Damage'] - [Slot 'Death']  - [Output Pose]
  • DefaultSlot: 공격 몽타주(AM_Khaimera_Attack)
  • Damage Slot: 피격 몽타주(Additive)
  • Death Slot: 사망 몽타주(AM_Khaimera_Death)

7. 비헤이비어 트리(BT) 및 블랙보드(BB) 구조

7-1. 비헤이비어 트리 노드 계층

BT_Khaimera
└─ Selector
   ├─ ⚙ Service: Find Player (DetectionRange: 1500)
   ├─ ⚙ Service: Drive Anim State (MoveSpeedThreshold: 50)
   │
   ├─ [1] Death Sequence (🔶 Decorator: bIsDead == True / Observer Aborts: Lower Priority)
   │     ├─ Wait (5.0s)
   │     └─ Destroy Owner Task
   │
   ├─ [2] Chase Sequence (🔶 Decorator: TargetPlayer Is Set / Observer Aborts: Both)
   │     ├─ MoveTo (TargetPlayer, Acceptable Radius: 100)
   │     ├─ Rotate To Face BB Entry (TargetPlayer)
   │     ├─ Melee Attack Task
   │     └─ Wait (2.5s - 공격 쿨다운)
   │
   └─ [3] Patrol Sequence
         ├─ Find Random Nav Location (Radius: 800)
         ├─ MoveTo (PatrolLocation)
         └─ Wait (2.0s)

7-2. 설정 시 주의사항

  1. MoveTo Acceptable Radius: 공격 사거리(도끼 길이)에 맞춰 100으로 설정합니다. 값이 너무 크면 거리 부족으로 헛스윙이 발생합니다.
  2. 사망 분기 데코레이터 설정: bIsDead 데코레이터의 Observer Aborts를 Lower Priority로 설정하여, 사망 이벤트 발생 시 즉시 하위 태스크(추격/공격)를 중단하고 사망 분기로 전환시킵니다.

8. 디버깅 및 체크리스트

현상 원인 및 해결 방법
공격을 수행하지 않음 MoveTo 사거리가 너무 길지 않은지 확인 (Radius = 100 권장), BT에 Drive Anim State 서비스가 등록되었는지 확인.
타격 성공 시 데미지 미적용 도끼 BoxComponent의 Collision Profile이 OverlapAllDynamic 및 Generate Overlap Events 설정인지 확인. AnimNotifyState 설정 범위 및 Side 파라미터 확인.
공격 모션 1회 후 멈춤 공격 몽타주 종료 시점에 AttackEnd 노티파이가 정상 배치되었는지, OnEndAttackNotify()를 호출하여 bIsAttack을 false로 재설정하는지 확인.
사망 후 다시 일어서는 현상 사망 몽타주(AM_Khaimera_Death)의 디테일 패널에서 Enable Auto Blend Out 항목을 False로 비활성화.

 


2. 심화반 내용

언리얼 C++ 스마트 포인터(Smart Pointer) 메모리 관리

 

1. 원시 포인터(Raw Pointer)의 한계와 스마트 포인터

C++의 원시 포인터(T*)를 사용한 수동 메모리 관리는 동적 할당된 메모리의 해제 시점을 개발자가 직접 제어해야 하므로 다음과 같은 치명적인 위험이 존재합니다.

  • 메모리 누수 (Memory Leak): delete를 호출하지 않아 메모리가 해제되지 않고 남아있는 현상.
  • 이중 해제 (Double Delete): 이미 해제된 메모리를 다시 delete하여 프로그램이 강제 종료되는 현상.
  • 허공 포인터 (Dangling Pointer): 이미 삭제된 메모리 주소에 접근(->)하여 크래시가 발생하는 현상.

스마트 포인터는 RAII(Resource Acquisition Is Initialization) 패턴을 기반으로 객체의 스코프(Scope)가 종료될 때 메모리를 자동으로 해제하여 이러한 위험을 방지합니다.

2. TSharedPtr & TSharedRef (공유 소유권)

2-1. TSharedPtr의 특징 및 동작 원리

TSharedPtr는 하나의 객체를 여러 곳에서 공동으로 소유할 수 있게 해주는 스마트 포인터입니다. 내부적으로 참조 카운팅(Reference Counting) 방식을 사용합니다.

  • 참조 컨트롤러 (FReferenceController): 실제 객체 포인터 외에 참조 카운트를 관리하는 컨트롤러를 별도로 가집니다.
    • SharedRefCount: 강한 참조(Strong Reference)의 개수. 0이 되면 객체가 자동 소멸합니다.
    • WeakRefCount: 약한 참조(Weak Reference)의 개수.
// 1. 생성 (MakeShared 권장)
TSharedPtr<FInventoryItem> Sword = MakeShared<FInventoryItem>("철검"); // SharedRefCount = 1

// 2. 복사 및 공유
TSharedPtr<FInventoryItem> AnotherSword = Sword; // SharedRefCount = 2

// 3. 유효성 검사 및 접근
if (Sword.IsValid())
{
    Sword->Use();
}

// 4. 해제
Sword = nullptr;        // SharedRefCount = 1
AnotherSword = nullptr; // SharedRefCount = 0 -> 객체 자동 소멸

 

2-2. MakeShared vs MakeShareable

  • MakeShared<T>() (권장): 객체 메모리와 FReferenceController 메모리를 단일 블록으로 동시 할당하여 메모리 파편화를 줄이고 성능을 최적화합니다.
  • MakeShareable(new T()): 이미 생성된 원시 포인터를 래핑하거나, 커스텀 디리터(Custom Deleter)를 지정할 때 제한적으로 사용합니다.

2-3. TSharedRef (절대 Null이 될 수 없는 참조)

TSharedRef는 TSharedPtr와 동일하게 참조 카운팅으로 동작하지만, Null 상태를 허용하지 않는 강한 참조입니다.

  • Null 체크(IsValid())가 불필요하여 코드가 간결해집니다.
  • TSharedRef → TSharedPtr는 암시적으로 변환됩니다.
  • TSharedPtr → TSharedRef 변환 시에는 .ToSharedRef()를 사용하며, 포인터가 유효한지 사전에 반드시 검사해야 합니다.
// TSharedRef 생성
TSharedRef<FQuest> QuestRef = MakeShared<FQuest>("드래곤 토벌");
QuestRef->StartQuest(); // Null 체크 없이 안전하게 바로 사용

// TSharedPtr -> TSharedRef 변환
TSharedPtr<FQuest> MaybeQuest = GetActiveQuest();
if (MaybeQuest.IsValid())
{
    TSharedRef<FQuest> SafeRef = MaybeQuest.ToSharedRef();
}

 

3. TWeakPtr (약한 참조 및 순환 참조 해결)

3-1. 순환 참조(Circular Reference) 문제

두 객체가 서로를 TSharedPtr로 강하게 참조하면, 참조 카운트가 절대로 0에 도달하지 못해 메모리가 영구적으로 누수됩니다.

// 순환 참조 예시: Parent와 Child가 서로 TSharedPtr를 가짐
class FParent { public: TSharedPtr<FChild> Child; };
class FChild  { public: TSharedPtr<FParent> Parent; }; // 🚨 메모리 누수 원인

3-2. TWeakPtr을 통한 해결 및 Pin() 메서드

TWeakPtr는 객체를 소유하지 않고 참조만 합니다. SharedRefCount를 증가시키지 않으므로 순환 참조를 끊어줍니다.

  • TWeakPtr로 참조 중인 객체에 접근하려면 Pin() 메서드를 호출하여 일시적으로 TSharedPtr로 승격시켜야 합니다.
  • Pin()이 성공하면 해당 스코프 내에서는 참조 카운트가 유지되므로 안전하게 접근 가능합니다.
class FUIWidget : public TSharedFromThis<FUIWidget>
{
private:
    TWeakPtr<FUIWidget> ParentWidget;       // 자식 -> 부모: 약한 참조
    TArray<TSharedPtr<FUIWidget>> Children; // 부모 -> 자식: 강한 참조

public:
    void NotifyParent(const FString& Message)
    {
        // Pin()을 통해 약한 참조 -> 강한 참조 승격 및 유효성 검사
        if (TSharedPtr<FUIWidget> ParentPin = ParentWidget.Pin())
        {
            ParentPin->ReceiveMessage(Message); // 안전하게 접근
        }
        else
        {
            // 부모 객체가 이미 소멸된 경우
            UE_LOG(LogTemp, Warning, TEXT("Parent widget no longer exists."));
        }
    }

    void AddChild(TSharedPtr<FUIWidget> Child)
    {
        Children.Add(Child);
        Child->ParentWidget = AsShared(); // TSharedFromThis를 통한 약한 참조 대입
    }
};

4. TUniquePtr (독점 소유권 및 RAII)

4-1. 특징

TUniquePtr는 해당 객체를 단 하나의 주체만 소유할 수 있도록 보장하는 스마트 포인터입니다.

  • 복사 불가: 복사 생성자 및 대입 연산자가 삭제되어 있습니다.
  • 이동 가능 (MoveTemp): MoveTemp를 통한 소유권 이전만 허용됩니다.
  • 최고의 성능: 참조 카운트 컨트롤러를 생성하지 않으므로 원시 포인터와 거의 동일한 메모리/속도 효율을 가집니다.
TUniquePtr<FWeapon> Weapon = MakeUnique<FWeapon>("엑스칼리버");

// TUniquePtr<FWeapon> Copied = Weapon; // ❌ 컴파일 에러 (복사 불가)
TUniquePtr<FWeapon> NewOwner = MoveTemp(Weapon); // ✅ 소유권 이동

// 이제 Weapon은 nullptr이 되며, NewOwner가 객체를 독점 소유함

4-2. 주요 활용 패턴: 텍스처/자원 관리자

class FTextureManager
{
private:
    TMap<FString, TUniquePtr<FTexture>> LoadedTextures;

public:
    void LoadTexture(const FString& TextureName)
    {
        TUniquePtr<FTexture> NewTexture = MakeUnique<FTexture>(TextureName);
        LoadedTextures.Add(TextureName, MoveTemp(NewTexture)); // 소유권 이동
    }

    FTexture* GetTexture(const FString& TextureName)
    {
        if (TUniquePtr<FTexture>* Found = LoadedTextures.Find(TextureName))
        {
            return Found->Get(); // 소유권은 넘기지 않고 원시 포인터(접근권)만 제공
        }
        return nullptr;
    }

    void UnloadTexture(const FString& TextureName)
    {
        // Map에서 제거되는 순간 TUniquePtr이 소멸되며 메모리 자동 해제
        LoadedTextures.Remove(TextureName);
    }
};

5. 스마트 포인터 선택 및 판별 가이드

5-1. 포인터 선택 흐름도

단계 질문 선택 결과
1단계 해당 클래스가 UObject 기반인가? UPROPERTY() 기반 원시 포인터 (TObjectPtr) 사용 (엔진 GC 관할)
2단계 단 한 곳에서만 독점적으로 소유하는가? TUniquePtr 사용
3단계 여러 곳에서 공유하며, 절대 Null이 될 수 없는가? TSharedRef 사용
4단계 여러 곳에서 공유하며, Null이 될 가능성이 있는가? TSharedPtr 사용
5단계 부모 참조, 리스너, 캐싱 등 순환 참조 위험이 있는가? TWeakPtr 사용

주의: 함수 인자로 스마트 포인터를 전달할 때 소유권을 넘기는 목적이 아니라면, 불필요한 참조 카운트 증감을 피하기 위해 const T& 또는 원시 포인터 T*로 전달하는 것이 성능상 유리합니다.

5-2. 실무 상황별 트레이드오프

  1. 설계가 유동적인 초기 단계:
    • 소유 관계가 명확해지기 전까지는 TSharedPtr로 안전하게 구현한 뒤, 구조가 확정되면 TUniquePtr + 원시 포인터 접근(Get()) 방식으로 최적화합니다.
  2. 레거시 코드 연동:
    • 외부 시스템이나 기존 C++ 원시 포인터 API와 연동할 때는 스마트 포인터의 소유권을 파괴하지 않는 선에서 .Get()을 통해 원시 포인터만 인자로 전달합니다.

 

 



*개인 프로젝트 및 개인 공부*

팀프로젝트를 하다보니 막판에 UI 작업을 하던 도중 여러 백엔드 작업을 동시에 진행하다보니 상당한 코드 오염과 충돌이 발생하기 시작했다. 이러한 문제의 원인이 무엇인지 파악하고 개선하기 위해서 게임 개발 과정을 좀 더 자세히 살펴보기로 했다.

 

--

 

팀 프로젝트에서 백엔드(서브시스템, 컴포넌트, 컨트롤러)의 데이터 흐름과 인터페이스 계약이 불명확할 때 발생하는 코드 오염(Code Pollution)과 결합도 증가 문제는 게임 개발 과정에서 매우 자주 발생하는 이슈이다. UI 작업자가 로직을 건드리거나, 반대로 백엔드 작업자가 UI 위젯 클래스를 직접 참조하면서 빌드 꼬임 및 머지 충돌이 터지게 된다.

이러한 문제를 방지하고 실무처럼 단방향 의존성(Event-Driven Architecture)인터페이스 중심 개발을 적용하는 방법과 순서를 알아보자.


1. 아키텍처 설계 & 작업 순서

 

cpp 코드를 작성하기 전에 백엔드 담당자와 UI 담당자가 약속(Interface Header)을 만드는 작업을 먼저 진행한다.

[1단계: 요구사항 분석 및 흐름 정의] 
       ↓ (시퀀스 다이어그램 작성)
[2단계: C++ 헤더 파일(.h) 인터페이스 선언] ➔ (Git Commit & PR: 백엔드/UI 헤더 합의)
       ↓ (병렬 작업 시작)
┌───────────────────────────────┴───────────────────────────────┐
│ [3-A: 백엔드 개발]                                              │ [3-B: UI/프론트 개발]
│ - Business Logic 구현                                          │ - WBP 위젯 레이아웃 제작
│ - 데이터 변동 시 Broadcast() 호출                               │ - Delegate 구독 및 UI Refresh 구현
└───────────────────────────────┬───────────────────────────────┘
       ↓ (합체)
[4단계: 통합 테스트 & 검증]

 

1단계: 기능 단위 시퀀스 다이어그램 작성

 

코드 작성 전, "누가 이벤트를 발생시키고(Trigger)", "누가 데이터의 주권(Single Source of Truth)을 가지며", "누가 결과를 수신하는가(Listener)"를 시퀀스 다이어그램으로 시각화한다.

 

2단계: 헤더 파일(.h) 중심의 계약(Contract) 선언

 

.cpp 구현부 없이 .h 파일에 다음 3가지만 선언한다.

 

데이터 전달용 델리게이트 (DECLARE_DYNAMIC_MULTICAST_DELEGATE)

외부 노출용 함수 (UFUNCTION(BlueprintCallable))

조회 전용 함수 (UFUNCTION(BlueprintPure))

이 헤더 선언이 끝난 시점에 기능 명세서가 완성된 것이므로, 이때부터 백엔드와 UI 작업자가 서로의 구현 상황을 기다리지 않고 병렬 개발을 수행할 수 있다.

 


 

2. 게임 개발에서의 백엔드 / 프론트엔드 주요 아키텍처

 

게임 개발, 특히 언리얼 엔진 기반 클라이언트에서의 프론트엔드와 백엔드는 역할과 레이어에 따라 구분된다.

 

1) 게임 개발의 레이어 구분 (프론트엔드(Presentation Layer), 클라이언트 백엔드(Domain/System Layer), 서버 백엔드(Server & Infra Layer))

 

[ Presentation Layer ]  ──>  UI, FX, Sound, Camera, Animation 연출
          │
[ Gameplay / Domain  ]  ──>  게임 규칙, FSM, 전투 연산, 물리, 입력 처리
          │
[ Data / Persistence ]  ──>  Data Table, Save/Load, 스탯/밸런스 데이터
          │
[ Backend / Server   ]  ──>  인증, DB, 동기화, 매치메이킹 (멀티플레이)

 

프론트엔드 : 플레이어의 눈에 보이는 화면 연출 및 입력을 수집한다. UI 위젯, UI 애니메이션, HUD, HUD 아이콘, 메인 메뉴, 입력 가공등의 역할을 한다. 스스로 게임 상태 데이터를 소유하지 않으며, 오직 '표현'과 '요청'만 담당한다. [Read-Only]

 

클라이언트 백엔드 : 게임의 핵심 규칙, 상태 제어, 데이터 연산을 담당한다. 서브시스템, 컴포넌트, 세이브 시스템의 요소를 주로 다룬다. UI의 존재를 몰라야 하며, 데이터의 주권(Single Source of Truth)을 가진다.

 

서버 백엔드 : 멀티플레이/온라인 게임에 해당하는 경우, 데이터베이스 연동, 계정 인증, 데디케이티드 서버 로직, 매치 메이킹 등의 역할을 담당한다.

 

2) 게임 개발 아키텍처 패턴 (이벤트 기반, 퍼사드, 액터-컴포넌트, MVC/MVP/MVVM, 상태 패턴, ECS, 커맨드 패턴, 오브젝트 풀링, 데이터 기반 아키텍처)

 

이벤트 기반 아키텍처 : 백엔드는 데이터 변동 발생 시 델리게이트로 방송만 한다. 그 이후에 UI가 이를 구독(AddDynamic)하여 화면을 갱신한다.

 

퍼사드 패턴 : 서브시스템이 복잡한 내부 데이터 연산을 숨기고, 외부에 간단한 인터페이스 함수만 제공하는 구조이다.

 

액터-컴포넌트 패턴 : 언리얼 엔진의 기본 구조로, 액터는 껍데기 및 제어 역할을 맡고 실제 기능은 부품 형태로 조립하여 결합도를 낮춘다.

 

MVC/MVP/MVVM : View(UI)와 Model(데이터) 사이에 Controller/Presenter/ViewModel을 두어 UI 연동 로직을 분리한다. 예를 들어, HUD UI가 플레이어 체력을 직접 가져오지 않고 ViewModel을 거쳐 바인딩하는 구조이다.

 

상태 패턴 (FSM) : 캐릭터나 게임의 상태를 클래스화하여 상태별 행동과 전환을 관리한다. 유닛 AI, 캐릭터 행동제어, 게임 루프 상태(Lobby -> InGame -> Result)를 관리하는 구조이다.

 

ECS(Entity-Component-System) : 객체지향의 한계를 극복하기 위해 데이터와 로직을 완전히 분리하는 게이터 지향 아키텍처이다. 화면에 등장하는 수많은 몬스터/탄막에 최적화된 구조이다.

 

커맨드 패턴 : 플레이어의 입력을 '명령 객체'로 재구성하여 재실행, 취소, 리플레이를 가능하게 한다. 리플레이 시스템, 키 매핑 변경, RTS 게임의 이동/공격 예약 명령 등에 사용되는 구조이다.

 

오브젝트 풀링 : 총알, 이펙트, 몬스터 등을 매번 생성/소멸시키지 않고 메모리에 미리 만들어두고 재사용한다. 잦은 GC 스파이크 및 프레임 드랍 방지를 최적화하는 구조이다.

 

데이터 기반 아키텍처 : 코드 수정 없이 데이터 변경만으로 게임 콘텐츠 및 수치를 조율할 수 있게 하는 아키텍처. 스킬 데미지, 아이템 옵션, 몬스터 스탯 밸런싱에 활용된다.


 

3. 게임 개발 직군별 역할 분담

직군 세부 직무 주요 담당 업무 Outputs
기획 시스템 기획자 게임 내 규칙, 서브시스템 구조, 경제 시스템 설계 기획데이터(Excel, JSON), 기획 명세서
레벨/전투 기획자 맵 배치, 몬스터 패턴, 캐릭터 수치 및 밸런싱 몬스터 스탯표, 레벨 블록아웃
UI/UX 기획자 와이어프레임 작성, 화면 흐름도(UX Flow) 정의 UI 와이어프레임, 기능 요구사항 명세
프로그래밍 클라이언트 시스템(백엔드) 서브시스템, 세이브/로드, 퀘스트/시간 데이터 처리 서브시스템 델리게이트 선언
게임플레이/휠 프로그래머 캐릭터 컨트롤러, 이동, 전투 판정, 컴포넌트 로직 컨트롤러, 액터컴포넌트
UI 프로그래머 UI 바인딩, 위젯 이벤트 처리, UI 상태 연동 UserWidget 클래스
테크니컬 프로그래머(TA) 엔진 최적화, 최적의 파이프라인 구축, 셰이더 작성 최적화 가이드, 머티리얼, 그래픽 파이프라인
아트 & 애니메이션 컨셉 / 3D 아트 캐릭터/원경 원화, 3D 모델링, 텍스처링 FBX 모델, 텍스처 데이터
애니메이터 플레이어, NPC, 몬스터 동작 및 모션 캡처 편집 Particle/Niagara Effect
VFX 아티스트 스킬 이펙트, 환경 피드백 연출 Animation Sequence, AnimMontage
UI 아티스트 위젯 디자인, 아이콘, 버튼 상태 이미지 제작 UI Texture, Sprite, Atlas
사운드 사운드 디자이너 BGM 작곡, SFX(효과음), 보이스 녹음/편 WAV 데이터, Sound Cue, Sound Attenuation
QA QA(Quaility Assurance) 기능 테스트, 엣지 케이스 점검, 버그 리포팅 테스트 케이스 문서, 버그 티켓

 


 

4. 시퀀스 다이어그램 작성을 위한 '요구사항 명세서'

 

먼저 기능 요구사항이 명확해야 한다. 요구사항은 "누가, 어떤 행동을 했을 때, 시스템이 어떻게 반응하며 무엇을 보여주는가"를 정의하는 단계이다.

 

1) 요구사항 명세서 필수 작성 6가지 항목

 

기능명 (Feature Name): 구현하고자 하는 기능 단위

 

트리거 / 입력 (Trigger / Input): 기능을 동작시키는 주체와 행동 (예: 버튼 클릭, 구역 진입)

 

데이터 주권자 (Single Source of Truth): 상태 및 데이터를 실제로 관리하는 주체

 

처리 로직 (Processing Logic): 백엔드에서 일어나는 내부 연산 단계

 

출력 및 이벤트 (Output & Broadcast): 결과 데이터 방송 및 UI 반영 방식

 

예외 처리 (Edge Cases): 조건 불만족 시 처리 (예: 골드 부족, 저장 실패 등)

 


5. QA 테스트 케이스 작성 및 관리 방법

 

개발 단계에서 QA 리스트를 미리 작성해 두어야 디버깅 스코프가 명확해지고 누락되는 엣지 케이스를 방지할 수 잇다. 이를 '기능 명세 기반 테스트 케이스 설계(Feature-Based TC)' 라고 한다.

 

1) 표준 QA 테스트 케이스 문서 구조 : QA 시트는 구글 시트나 노션 테이블 형태로 팀원 전체가 공유할 수 있도록 한다.

 

<항목(Column)별 정리>

 

TC-ID : 모듈별 고유 식별자를 넣는다. 예를 들어 UI-DAY-001 형식.

 

기능 분류 : 테스트 대상 시스템 및 화면. 예를 들어, HUD.

 

사전 조건(Precondition) : 테스트 실행을 위한 최소 준비 상태. 예를 들어, 게임 진입 완료, 현재 날짜, 보유 골드, 요구 레벨 등

 

테스트 스텝(Steps) : 순차적인 플레이어 행동. 예를 들어, HUD 상단의 메뉴 버튼 클릭 후 옵션 버튼 클릭, 팝업창의 확인 버튼 클릭 등.

 

기대 결과(Expected) : 정상 동작 시 발생해야 하는 모든 결과. 예를 들어, UI 날짜가 변경되고, 플레이어 개인 설정이 변경되고, 골드가 차감되고, 자동 저장 진행 등.

 

결과(Status) : Pass/Fail/Blocked. 결과를 적어넣으면 된다. 나중에 체크하는 용도.

 

비고/이슈 티켓 : 버그 발생 시 이슈 링크 및 로그를 작성한다. 예를 들어 #BUG-104 (골드가 음수가 되는 현상) 이렇게 적어넣는 것.

 

2) 단계별 QA 관리 프로세스

[1단계: 요구사항 정리] ──> [2단계: TC 미리 작성] ──> [3단계: 기능 구현] ──> [4단계: 스모크 & 예외 테스트]

 

Step 1. 요구사항 정리 및 엣지 케이스 추출

 

기능을 만들기 전 "정상 흐름"과 "예외 흐름(Edge Case)"을 함께 정리한다.

정상 흐름: 골드가 충분할 때 하루 마감 버튼을 누르면 다음 날로 넘어가고 골드가 차감된다.

예외 흐름: 골드가 부족할 때 누르면 어떻게 되는가? 마감 버튼을 0.1초 만에 연속으로 10번 클릭(Spamming)하면 어떻게 되는가?

 

Step 2. 스모크 테스트(Smoke Test) 체계 구축

 

디버깅에 시간을 빼앗기지 않으려면 스모크 테스트 단계가 필수적이다.

스모크 테스트: 상세 테스트에 들어가기 전, 빌드가 튕기지 않고 핵심 기능이 켜지는지 3~5분 내에 확인하는 최소한의 널 체크(Null Check) 과정이다.

 

Step 3. 테스트 진행 및 버그 리포팅

 

테스트 결과 중 Fail이 난 항목은 즉시 아래 요소를 적어 담당 프로그래머에게 공유한다..

<공유 사항>

재현 절차 (Reproduction Steps)

재현율 (예: 10번 중 10번 100% 발생 / 3번 중 1번 발생)

해당 시점의 로그(Log) 파일 및 화면 캡처/영상

 



*오늘의 총평*

바쁜하루였다.

'TIL' 카테고리의 다른 글

07.23 TIL  (0) 2026.07.23
07.22 TIL  (0) 2026.07.22
07.20 TIL  (0) 2026.07.20
07.19 TIL  (0) 2026.07.19
07.18 TIL  (0) 2026.07.18