언리얼 엔진 5.X의 Windows 11 한글 IME 입력 문제 해결

Mazeline·2025년 10월 29일
0

TA 가이드

목록 보기
8/9
post-thumbnail

들어가기

이 해결은 메이즈라인 고객사 중 하나인 “게임테일즈” 클라이언트실/테크아트팀/최성현 사원이 외부 참조 리퀘스트 자료를 참조 하고 해결하는 과정을 소개 하고 있습니다.

언리얼 엔진 5.0(UE5)을 Windows 11 환경에서 사용하는 과정에서 한글 및 중국어와 같은 다국어 입력 시스템(IME, Input Method Editor)과 관련된 특정 입력 오류가 발견되었습니다. 특히, 한글 입력 중 스페이스바를 통한 띄어쓰기 후 자음이나 모음을 입력할 때 정상적으로 조합되지 않는 현상이 지속적으로 보고되었습니다.

본 문서에서는 이러한 문제의 근본 원인을 분석하고, 엔진 소스 코드 레벨에서 적용한 해결 방안을 상세히 기술하고자 합니다.

Windows 11 IME 아키텍처의 변화와 문제의 발생 원인

새로운 IME 시스템의 특성

Windows 11은 이전 버전들과 비교하여 입력 메서드 시스템(IME)에 상당한 변화를 도입하였습니다. 특히 Text Services Framework(TSF)의 구현 방식이 개선되면서, IME와 애플리케이션 간의 통신 프로토콜이 보다 엄격해졌습니다.

핵심적인 문제는 TSF의 ITextStoreACP 인터페이스 구현에서 발생합니다. Windows 11의 새로운 IME는 다음과 같은 특성을 보입니다:

  1. 빈 텍스트 범위(Empty Range) 쿼리 증가: 새로운 IME는 입력 컨텍스트를 파악하기 위해 acpStart == acpEnd인 경우도 빈번하게 요청합니다.
  2. 텍스트 경계 계산의 엄격화: 커서 위치와 조합 중인 텍스트의 위치를 계산할 때 라인 높이(Line Height)를 포함한 정확한 바운딩 박스를 요구합니다.
  3. 텍스트 변경 통보의 필요성: IME가 조합 상태를 올바르게 유지하려면 애플리케이션 측에서 텍스트 변경 사항을 명시적으로 통보해야 합니다.

언리얼 엔진 5.0의 초기 구현은 Windows 10 이하의 IME 동작 방식을 기준으로 설계되었기 때문에, 이러한 새로운 요구사항에 대응하지 못했습니다.

구체적인 오류 시나리오

사용자가 한글을 입력하는 과정을 단계별로 살펴보겠습니다:

  1. 사용자가 "안녕"을 입력합니다.
  2. 스페이스바를 눌러 띄어쓰기를 합니다.
  3. 다음 단어의 첫 자음 "ㅎ"를 입력합니다.

이 시점에서 Windows 11의 IME는 다음과 같은 작업을 수행합니다:

  • 현재 커서 위치의 텍스트 범위를 요청 (GetTextExt 호출, acpStart == acpEnd)
  • 조합 윈도우를 표시할 위치를 계산하기 위해 텍스트 바운딩 박스를 요청

그러나 언리얼 엔진의 기존 구현은:

  • acpStart == acpEnd인 경우 E_INVALIDARG 오류를 반환
  • 텍스트 바운딩 박스 계산 시 라인 높이를 고려하지 않아 부정확한 위치 반환

이로 인해 IME가 조합 상태를 정상적으로 초기화하지 못하고, 입력된 자모가 별도의 문자로 처리되는 현상이 발생합니다.

이전 버전 IME 사용 시 문제가 해결되는 이유

많은 사용자들이 Windows 설정에서 "이전 버전의 Microsoft IME 사용" 옵션을 활성화하면 문제가 해결된다는 것을 발견했습니다. 이는 다음과 같은 이유 때문입니다:

호환성 모드의 동작 방식

이전 버전의 IME(Windows 10 스타일)는:

  1. 느슨한 에러 핸들링: acpStart == acpEnd인 경우에도 유효한 요청으로 처리하거나, 오류를 받더라도 대체 로직을 사용합니다.
  2. 단순화된 위치 계산: 텍스트 바운딩 박스 계산이 덜 엄격하며, 근사값을 허용합니다.
  3. 비동기 통보의 유연성: 텍스트 변경 통보가 누락되어도 IME 자체적으로 상태를 재동기화하는 로직을 포함합니다.

따라서 언리얼 엔진의 불완전한 TSF 구현에도 불구하고, 이전 버전 IME는 자체적인 fallback 메커니즘을 통해 정상적으로 동작할 수 있었습니다.

그러나 이는 근본적인 해결책이 아니며, Windows 11의 새로운 IME가 제공하는 향상된 입력 경험과 성능을 활용할 수 없다는 한계가 있습니다.

엔진 소스 코드 수정 사항

문제를 근본적으로 해결하기 위해 IME와 언리얼 엔진 간의 통신 경로를 추적했습니다. Windows의 Text Services Framework(TSF)는 계층적 구조로 동작하는데, 에러가 발생하는 지점을 역추적 할 수 있습니다.

문제 진단 과정

문제 해결을 위해 IME와 언리얼 엔진 간의 통신 경로를 계층별로 추적했습니다. 가장 먼저 발견한 증상은 한글 입력 시 IME 조합 창이 부적절한 위치에 표시되거나 아예 표시되지 않는 현상이었습니다. TSF 디버깅을 통해 GetTextExt 메서드에서 E_INVALIDARG 오류가 반환되는 것을 확인했고, TSF의 핵심 인터페이스인 ITextStoreACP를 구현하는 TextStoreACP.cpp를 분석한 결과 acpStart == acpEnd 조건을 엄격하게 거부하는 로직이 Windows 11의 새로운 IME 동작 방식과 충돌하는 것을 발견했습니다.

그러나 TextStoreACP의 수정만으로는 문제가 완전히 해결되지 않았습니다. 간헐적으로 IME가 아예 동작하지 않는 경우가 있었는데, 이는 초기화 순서 문제입니다. Windows 11에서 IME 시스템은 유효한 윈도우 핸들이 존재할 때만 정상적으로 초기화되는데, FWindowsApplication의 생성자에서 윈도우 생성 전에 TextInputMethodSystem을 초기화하려는 코드가 원인이었습니다. 초기화 위치를 InitializeWindow 함수로 옮기는 것만으로 안정성을 확보할 수 있습니다.

마지막으로 남은 문제는 스페이스바 입력 후 다음 자모 입력이 제대로 처리되지 않는 현상이었습니다. 언리얼 엔진의 UI 프레임워크인 Slate가 실제 텍스트 입력을 처리하는 레이어로, SlateEditableTextLayout.cpp를 분석한 결과 두 가지 문제를 발견했습니다. 첫째, 텍스트 편집이 완료될 때 IME에게 명시적인 통보(NotifyTextChanged)를 하지 않았고, 둘째, 텍스트 바운딩 박스를 계산할 때 라인 높이를 고려하지 않아 IME가 부정확한 위치 정보를 받고 있었습니다. 이 두 가지를 수정하여 문제를 해결할 수 있습니다.

수정 대상 파일의 역할 정리

결과적으로, 세 개의 파일은 각각 다음과 같은 계층에서 문제를 해결합니다:

  1. TextStoreACP.cpp (TSF 프로토콜 레벨): Windows와의 저수준 통신 프로토콜 준수
  2. WindowsApplication.cpp (애플리케이션 생명주기 레벨): IME 시스템의 올바른 초기화 순서 보장
  3. SlateEditableTextLayout.cpp (UI 레벨): 사용자 입력을 받아 IME와 동기화

이 세 레이어가 모두 올바르게 동작해야만 Windows 11의 엄격해진 IME 시스템과 완벽하게 호환될 수 있었습니다.


구체적인 코드 수정 내역

문제를 근본적으로 해결하기 위해 다음 세 개의 핵심 파일을 수정하였습니다.

1. TextStoreACP.cpp의 수정

TextStoreACP는 TSF의 ITextStoreACP 인터페이스를 구현하는 클래스로, IME와 애플리케이션 간의 텍스트 데이터 교환을 담당합니다.

1.1 GetTextExt 함수의 개선

기존 코드의 문제점:

const LONG StringLength = TextContext->GetTextLength();

// Begin and end indices must not be equal.
if(acpStart == acpEnd)
{
    return E_INVALIDARG;
}

// Validate range.
if( acpStart < 0 || acpStart > StringLength || ( acpEnd != -1 && ( acpEnd < 0 || acpEnd > StringLength) ) )
{
    return TS_E_INVALIDPOS;
}

이 코드는 acpStart == acpEnd인 경우를 명시적으로 오류로 처리했습니다. 그러나 Windows 11 IME는 커서 위치의 텍스트 범위를 확인하기 위해 이러한 쿼리를 정당하게 사용합니다.

수정된 코드:

const LONG StringLength = TextContext->GetTextLength();
// Validate range.
if( acpStart < 0 || acpStart > StringLength || ( acpEnd != -1 && ( acpEnd < 0 || acpEnd > StringLength) ) )
{
    return TS_E_INVALIDPOS;
}

빈 범위 검증을 제거함으로써, IME가 커서 위치의 바운딩 박스를 정상적으로 조회할 수 있게 되었습니다.

1.2 GetText 함수의 최적화

기존 코드의 문제점:

const LONG StringLength = TextContext->GetTextLength();

// Validate range.
if( acpStart < 0 || acpStart > StringLength || ( acpEnd != -1 && ( acpEnd < 0 || acpEnd > StringLength) ) )
{
    return TF_E_INVALIDPOS;
}

const uint32 BeginIndex = acpStart;
const uint32 Length = (acpEnd == -1 ? StringLength : acpEnd) - BeginIndex;

for(uint32 i = 0; i < Length && *pcchPlainOut < cchPlainReq; ++i)
{
    pchPlain[i] = StringInRange[i];
    ++(*pcchPlainOut);
}

이 구현에는 두 가지 문제가 있었습니다:

  1. acpEnd == -1의 처리가 검증 이후에 이루어져 로직이 복잡함
  2. 버퍼 크기(cchPlainReq)를 고려하지 않고 Length를 계산하여 버퍼 오버플로우 가능성 존재

수정된 코드:

const LONG StringLength = TextContext->GetTextLength();
if (acpEnd == -1)
{
    acpEnd = StringLength;
}
// Validate range.
if( acpStart < 0 || acpStart > StringLength || acpEnd < 0 || acpEnd > StringLength )
{
    return TF_E_INVALIDPOS;
}

const uint32 BeginIndex = acpStart;
const uint32 Length = FMath::Min(cchPlainReq, acpEnd - BeginIndex);

for(uint32 i = 0; i < Length; ++i)
{
    pchPlain[i] = StringInRange[i];
    ++(*pcchPlainOut);
}

주요 개선 사항:

  • acpEnd == -1을 먼저 처리하여 검증 로직을 단순화
  • FMath::Min을 사용하여 버퍼 크기를 초과하지 않도록 안전장치 추가
  • 루프 조건에서 불필요한 중복 검사 제거로 성능 향상

1.3 Run Info 처리 개선

수정된 코드:

if(prgRunInfo && ulRunInfoReq > 0 && Length > 0)
{
    prgRunInfo[0].uCount = Length;
    prgRunInfo[0].type = TS_RT_PLAIN;
    ++(*pulRunInfoOut);
}

Length > 0 검증을 추가하여, 빈 텍스트 범위에 대해서도 안전하게 처리할 수 있도록 개선하였습니다.

2. WindowsApplication.cpp의 수정

텍스트 입력 메서드 시스템의 초기화 시점 변경

기존 코드의 문제점:

// FWindowsApplication 생성자에서
OleInitialize( NULL );

#if !USING_ADDRESS_SANITISER
TextInputMethodSystem = MakeShareable( new FWindowsTextInputMethodSystem );
if(!TextInputMethodSystem->Initialize())
{
    TextInputMethodSystem.Reset();
}
#endif

FWindowsApplication의 생성자에서 TextInputMethodSystem을 초기화하면, 아직 윈도우가 생성되지 않은 상태에서 IME 컨텍스트를 설정하려고 시도할 수 있습니다. 이는 Windows 11에서 IME 초기화 실패로 이어질 수 있습니다.

수정된 코드:

// InitializeWindow 함수에서
Windows.Add( Window );
Window->Initialize( this, InDefinition, InstanceHandle, ParentWindow, bShowImmediately );

#if !USING_ADDRESS_SANITISER
static const bool InitInputMethodSystem = [this] {
    TextInputMethodSystem = MakeShareable(new FWindowsTextInputMethodSystem);
    if (!TextInputMethodSystem->Initialize())
    {
        TextInputMethodSystem.Reset();
    }
    return true;
}();
#endif

주요 개선 사항:

  • 윈도우 초기화 완료 후에 TextInputMethodSystem을 생성
  • static 람다를 사용하여 전체 애플리케이션 생명주기 동안 단 한 번만 초기화되도록 보장
  • 윈도우 핸들이 유효한 상태에서 IME 컨텍스트가 생성되므로, 안정성이 크게 향상됨

이 변경은 특히 중요한데, Windows 11의 IME는 유효한 윈도우 핸들과의 연결을 필수적으로 요구하기 때문입니다.

3. SlateEditableTextLayout.cpp의 수정

3.1 포커스 손실 시 조합 취소 처리

수정된 코드:

bool FSlateEditableTextLayout::HandleFocusLost(const FFocusEvent& InFocusEvent)
{
    // ... 기존 코드 ...
    
    ITextInputMethodSystem* const TextInputMethodSystem = FSlateApplication::Get().GetTextInputMethodSystem();
    if (TextInputMethodSystem && bHasRegisteredTextInputMethodContext)
    {
        if (TextInputMethodContext->IsComposing() && TextInputMethodChangeNotifier.IsValid())
        {
            TextInputMethodChangeNotifier->CancelComposition();
        }
        TextInputMethodSystem->DeactivateContext(TextInputMethodContext.ToSharedRef());
    }
}

에디터블 텍스트 위젯이 포커스를 잃을 때, 진행 중인 조합(Composition)이 있다면 명시적으로 취소합니다. 이는 다른 위젯으로 포커스가 이동할 때 IME 상태가 올바르게 정리되도록 보장합니다.

3.2 커서 하이라이트 로직 수정

기존 코드:

if (/* 조합 중 */) {
    // 조합 중인 텍스트 하이라이트
}
else if (bHasSelection) {
    // 선택 영역 하이라이트
}

수정된 코드:

if (/* 조합 중 */) {
    // 조합 중인 텍스트 하이라이트
}

if (bHasSelection) {
    // 선택 영역 하이라이트
}

else ifif로 변경하여, 조합 중인 텍스트와 선택 영역이 동시에 표시될 수 있도록 수정했습니다. 이는 일부 IME가 조합 중에도 텍스트 선택을 유지하는 경우를 올바르게 처리합니다.

3.3 텍스트 변경 통보 추가

수정된 코드:

void FSlateEditableTextLayout::EndEditTransaction()
{
    // ... 기존 코드 ...
    
    SaveText(EditedText);

    if (TextInputMethodChangeNotifier.IsValid() && TextInputMethodContext.IsValid())
    {
        uint32 OldLen = StateBeforeChangingText.GetValue().Text.ToString().Len();
        uint32 NewLen = EditedText.ToString().Len();
        TextInputMethodChangeNotifier->NotifyTextChanged(0, OldLen, NewLen);
    }
    
    PushUndoState(StateBeforeChangingText.GetValue());
}

텍스트 편집이 완료될 때마다 IME에게 변경 사항을 명시적으로 통보합니다. 이는 IME가 내부 상태를 올바르게 동기화할 수 있도록 하며, 특히 스페이스바 입력 후 다음 문자를 입력할 때 발생하던 문제를 해결하는 핵심 수정입니다.

3.4 텍스트 바운딩 박스 계산 개선

기존 코드:

const FVector2D BeginPosition = OwnerLayout->TextLayout->GetLocationAt(BeginLocation, false);
const FVector2D EndPosition = OwnerLayout->TextLayout->GetLocationAt(EndLocation, false);

if (BeginPosition.Y == EndPosition.Y)
{
    Position = BeginPosition;
    Size = EndPosition - BeginPosition;
}
else
{
    Position = FVector2D(0.0f, BeginPosition.Y);
    Size = FVector2D(OwnerLayout->TextLayout->GetDrawSize().X, EndPosition.Y - BeginPosition.Y);
}

수정된 코드:

const FVector2D BeginPosition = OwnerLayout->TextLayout->GetLocationAt(BeginLocation, false);
const FVector2D EndPosition = OwnerLayout->TextLayout->GetLocationAt(EndLocation, false);

const TArray< FTextLayout::FLineView >& LineViews = OwnerLayout->TextLayout->GetLineViews();
int32 LineViewIndex = OwnerLayout->TextLayout->GetLineViewIndexForTextLocation(LineViews, BeginLocation, false);
double BeginLineHeight{};
if (LineViewIndex != INDEX_NONE)
{
    BeginLineHeight = LineViews[LineViewIndex].Size.Y;
}

if (BeginPosition.Y == EndPosition.Y)
{
    Position = FVector2D(BeginPosition.X, BeginPosition.Y - BeginLineHeight);
    Size = EndPosition - Position;
}
else
{
    Position = FVector2D(0.0f, BeginPosition.Y - BeginLineHeight);
    Size = FVector2D(OwnerLayout->TextLayout->GetDrawSize().X, EndPosition.Y - Position.Y);
}

주요 개선 사항:

  • 해당 라인의 높이(BeginLineHeight)를 계산에 포함
  • Y 좌표를 라인 상단부터 시작하도록 조정 (BeginPosition.Y - BeginLineHeight)
  • 이를 통해 IME 조합 윈도우가 텍스트의 정확한 위치에 표시됨

Windows 11 IME는 조합 윈도우의 위치를 결정하기 위해 매우 정확한 바운딩 박스 정보를 필요로 합니다. 기존 구현은 베이스라인 위치만 제공했지만, 수정된 코드는 텍스트의 전체 높이를 포함한 정확한 영역을 제공합니다.

결론

본 수정 사항들은 언리얼 엔진 5.0이 Windows 11의 새로운 IME 아키텍처와 완전히 호환되도록 만듭니다. 정리 해 보면 다음과 같습니다:

  1. TSF 프로토콜의 완전한 준수: ITextStoreACP 인터페이스를 Windows 11 IME의 요구사항에 맞게 올바르게 구현
  2. 안정성 향상: 초기화 순서 개선으로 IME 시스템의 안정성 확보
  3. 사용자 경험 개선: 한글, 중국어 등 복잡한 입력 시스템에서도 자연스러운 입력 경험 제공
  4. 성능 최적화: 불필요한 검증 제거 및 효율적인 버퍼 관리

이러한 수정을 통해 사용자들은 더 이상 "이전 버전의 IME 사용" 옵션에 의존할 필요 없이, Windows 11의 최신 입력 기능을 완전히 활용할 수 있게 되었습니다.

게임 엔진과 운영체제의 입력 시스템 통합은 사용자 경험의 근간을 이루는 중요한 요소입니다. 본 사례는 플랫폼 업데이트에 따른 호환성 이슈를 체계적으로 분석하고 해결하는 과정을 보여주며, 향후 유사한 문제에 대한 참고 자료로 활용될 수 있을 것입니다.


profile
테크아트 컨설팅 전문 회사 "메이즈라인" 입니다.

0개의 댓글