
이 해결은 메이즈라인 고객사 중 하나인 “게임테일즈” 클라이언트실/테크아트팀/최성현 사원이 외부 참조 리퀘스트 자료를 참조 하고 해결하는 과정을 소개 하고 있습니다.
언리얼 엔진 5.0(UE5)을 Windows 11 환경에서 사용하는 과정에서 한글 및 중국어와 같은 다국어 입력 시스템(IME, Input Method Editor)과 관련된 특정 입력 오류가 발견되었습니다. 특히, 한글 입력 중 스페이스바를 통한 띄어쓰기 후 자음이나 모음을 입력할 때 정상적으로 조합되지 않는 현상이 지속적으로 보고되었습니다.
본 문서에서는 이러한 문제의 근본 원인을 분석하고, 엔진 소스 코드 레벨에서 적용한 해결 방안을 상세히 기술하고자 합니다.
Windows 11은 이전 버전들과 비교하여 입력 메서드 시스템(IME)에 상당한 변화를 도입하였습니다. 특히 Text Services Framework(TSF)의 구현 방식이 개선되면서, IME와 애플리케이션 간의 통신 프로토콜이 보다 엄격해졌습니다.
핵심적인 문제는 TSF의 ITextStoreACP 인터페이스 구현에서 발생합니다. Windows 11의 새로운 IME는 다음과 같은 특성을 보입니다:
acpStart == acpEnd인 경우도 빈번하게 요청합니다.언리얼 엔진 5.0의 초기 구현은 Windows 10 이하의 IME 동작 방식을 기준으로 설계되었기 때문에, 이러한 새로운 요구사항에 대응하지 못했습니다.
사용자가 한글을 입력하는 과정을 단계별로 살펴보겠습니다:
이 시점에서 Windows 11의 IME는 다음과 같은 작업을 수행합니다:
GetTextExt 호출, acpStart == acpEnd)그러나 언리얼 엔진의 기존 구현은:
acpStart == acpEnd인 경우 E_INVALIDARG 오류를 반환이로 인해 IME가 조합 상태를 정상적으로 초기화하지 못하고, 입력된 자모가 별도의 문자로 처리되는 현상이 발생합니다.
많은 사용자들이 Windows 설정에서 "이전 버전의 Microsoft IME 사용" 옵션을 활성화하면 문제가 해결된다는 것을 발견했습니다. 이는 다음과 같은 이유 때문입니다:
이전 버전의 IME(Windows 10 스타일)는:
acpStart == acpEnd인 경우에도 유효한 요청으로 처리하거나, 오류를 받더라도 대체 로직을 사용합니다.따라서 언리얼 엔진의 불완전한 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가 부정확한 위치 정보를 받고 있었습니다. 이 두 가지를 수정하여 문제를 해결할 수 있습니다.
결과적으로, 세 개의 파일은 각각 다음과 같은 계층에서 문제를 해결합니다:
이 세 레이어가 모두 올바르게 동작해야만 Windows 11의 엄격해진 IME 시스템과 완벽하게 호환될 수 있었습니다.
문제를 근본적으로 해결하기 위해 다음 세 개의 핵심 파일을 수정하였습니다.
TextStoreACP는 TSF의 ITextStoreACP 인터페이스를 구현하는 클래스로, IME와 애플리케이션 간의 텍스트 데이터 교환을 담당합니다.
기존 코드의 문제점:
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가 커서 위치의 바운딩 박스를 정상적으로 조회할 수 있게 되었습니다.
기존 코드의 문제점:
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);
}
이 구현에는 두 가지 문제가 있었습니다:
acpEnd == -1의 처리가 검증 이후에 이루어져 로직이 복잡함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을 사용하여 버퍼 크기를 초과하지 않도록 안전장치 추가수정된 코드:
if(prgRunInfo && ulRunInfoReq > 0 && Length > 0)
{
prgRunInfo[0].uCount = Length;
prgRunInfo[0].type = TS_RT_PLAIN;
++(*pulRunInfoOut);
}
Length > 0 검증을 추가하여, 빈 텍스트 범위에 대해서도 안전하게 처리할 수 있도록 개선하였습니다.
기존 코드의 문제점:
// 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 람다를 사용하여 전체 애플리케이션 생명주기 동안 단 한 번만 초기화되도록 보장이 변경은 특히 중요한데, Windows 11의 IME는 유효한 윈도우 핸들과의 연결을 필수적으로 요구하기 때문입니다.
수정된 코드:
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 상태가 올바르게 정리되도록 보장합니다.
기존 코드:
if (/* 조합 중 */) {
// 조합 중인 텍스트 하이라이트
}
else if (bHasSelection) {
// 선택 영역 하이라이트
}
수정된 코드:
if (/* 조합 중 */) {
// 조합 중인 텍스트 하이라이트
}
if (bHasSelection) {
// 선택 영역 하이라이트
}
else if를 if로 변경하여, 조합 중인 텍스트와 선택 영역이 동시에 표시될 수 있도록 수정했습니다. 이는 일부 IME가 조합 중에도 텍스트 선택을 유지하는 경우를 올바르게 처리합니다.
수정된 코드:
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가 내부 상태를 올바르게 동기화할 수 있도록 하며, 특히 스페이스바 입력 후 다음 문자를 입력할 때 발생하던 문제를 해결하는 핵심 수정입니다.
기존 코드:
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)를 계산에 포함BeginPosition.Y - BeginLineHeight)Windows 11 IME는 조합 윈도우의 위치를 결정하기 위해 매우 정확한 바운딩 박스 정보를 필요로 합니다. 기존 구현은 베이스라인 위치만 제공했지만, 수정된 코드는 텍스트의 전체 높이를 포함한 정확한 영역을 제공합니다.
본 수정 사항들은 언리얼 엔진 5.0이 Windows 11의 새로운 IME 아키텍처와 완전히 호환되도록 만듭니다. 정리 해 보면 다음과 같습니다:
ITextStoreACP 인터페이스를 Windows 11 IME의 요구사항에 맞게 올바르게 구현이러한 수정을 통해 사용자들은 더 이상 "이전 버전의 IME 사용" 옵션에 의존할 필요 없이, Windows 11의 최신 입력 기능을 완전히 활용할 수 있게 되었습니다.
게임 엔진과 운영체제의 입력 시스템 통합은 사용자 경험의 근간을 이루는 중요한 요소입니다. 본 사례는 플랫폼 업데이트에 따른 호환성 이슈를 체계적으로 분석하고 해결하는 과정을 보여주며, 향후 유사한 문제에 대한 참고 자료로 활용될 수 있을 것입니다.