Open Source

IME 입력 중 다른 텍스트 영역 이동 시 이전 스타일이 유지되는 문제 해결

2026년 2월 17일

Lexical 에디터에서 IME(한글 등) 입력 도중 다른 텍스트 영역으로 커서를 이동했을 때, 이전 영역의 포맷(Bold 등)이 그대로 유지되는 버그를 분석하고 해결한 과정을 공유합니다.

문제 상황

특정 포맷(예: Bold)이 적용된 텍스트를 IME로 입력하던 중, 조합을 완료하지 않은 상태에서 일반 포맷의 다른 영역으로 커서를 이동하여 입력을 이어갈 때 스타일이 올바르게 업데이트되지 않는 현상이 발생했습니다.

재현 단계

  1. Bold 포맷 영역에 텍스트 "가다" 입력
  2. 일반 포맷 영역에 텍스트 "라바" 입력
  3. "가다"에서 "가"와 "다" 사이에 "나"를 입력하며 (엔터나 방향키로 조합을 완료하지 않음)
  4. "라바" 영역의 "라"와 "바" 사이를 클릭하여 커서를 이동
  5. "마" 문자 입력

결과: 새로 입력한 "마"에 일반 포맷이 아닌, 이전 영역의 Bold 포맷이 적용됩니다.


원인 분석

디버깅

에디터 내부의 Selection 상태를 추적해본 결과, 다음과 같은 사실을 발견했습니다.

  • 일반적인 영문 입력이나 조합이 완료된 상태에서는 커서 이동 시 해당 위치의 노드 포맷으로 Selection의 formatstyle이 갱신됩니다.
  • 하지만 IME 조합(Composition) 중에는 커서가 이동하더라도 Lexical의 RangeSelection이 이전 상태의 포맷 정보를 그대로 들고 있는 문제가 있었습니다.

코드 분석

문제가 발생한 지점은 Lexical 내부에서 Selection을 생성하는 $internalCreateRangeSelection 함수와, Selection 포인트를 정규화하는 $normalizeSelectionPointsForBoundaries 함수였습니다.

1. $normalizeSelectionPointsForBoundaries의 강제 복원 로직

IME 조합 중에는 Selection이 변경되더라도 이를 강제로 이전 상태로 되돌리는 로직이 존재했습니다.

if (
  editor.isComposing() &&
  editor._compositionKey !== anchor.key &&
  $isRangeSelection(lastSelection)
) {
  const lastAnchor = lastSelection.anchor;
  const lastFocus = lastSelection.focus;
  anchor.set(lastAnchor.key, lastAnchor.offset, lastAnchor.type, true);
  focus.set(lastFocus.key, lastFocus.offset, lastFocus.type, true);
}

이 로직으로 인해 IME 입력 중 다른 노드를 클릭해도 Selection의 위치(anchor, focus)가 이전 노드에 묶여 있게 됩니다.

2. $internalCreateRangeSelection의 스타일 재사용

또한, 새로운 RangeSelection을 생성할 때 이전 Selection의 포맷 정보를 재사용하고 있었습니다.

export function $internalCreateRangeSelection(
  lastSelection: null | BaseSelection,
  domSelection: Selection | null,
  editor: LexicalEditor,
  event: UIEvent | Event | null,
): null | RangeSelection {
  // ... (중략) ...

  const [resolvedAnchorPoint, resolvedFocusPoint] = resolvedSelectionPoints;

  // 기존의 format과 style은 유지하면서 anchor와 focus만 변경하고 있음
  return new RangeSelection(
    resolvedAnchorPoint,
    resolvedFocusPoint,
    !$isRangeSelection(lastSelection) ? 0 : lastSelection.format,
    !$isRangeSelection(lastSelection) ? '' : lastSelection.style,
  );
}

새로운 RangeSelection을 생성할 때 lastSelection이 존재하면 그곳의 formatstyle을 재사용하고 있었습니다. 이로 인해 IME 조합 중 커서가 이동하여 새로운 노드에 위치하게 되어도 스타일 정보는 갱신되지 않았습니다.


해결 방법

1. $normalizeSelectionPointsForBoundaries 수정

IME 조합 중 노드가 변경될 때 Selection을 강제로 이전 상태로 되돌리는 로직을 제거했습니다. 이를 통해 커서 이동 시 Selection이 새로운 노드를 정확히 가리킬 수 있게 되었습니다.

2. Selection 생성 시 스타일 동기화 로직 추가

$internalCreateRangeSelection 함수에서 노드의 변경 여부를 확인하고 스타일을 동기화하는 로직을 추가했습니다.

const [resolvedAnchorPoint, resolvedFocusPoint] = resolvedSelectionPoints;
let format = 0;
let style = "";

if ($isRangeSelection(lastSelection)) {
  const lastAnchor = lastSelection.anchor;

  // 노드가 이전과 동일하다면 기존 스타일 유지
  if (resolvedAnchorPoint.key === lastAnchor.key) {
    format = lastSelection.format;
    style = lastSelection.style;
  }
  // 노드가 변경되었다면 새로운 노드의 포맷 정보를 가져옴
  else {
    const anchorNode = resolvedAnchorPoint.getNode();
    if ($isTextNode(anchorNode)) {
      format = anchorNode.getFormat();
      style = anchorNode.getStyle();
    } else if ($isElementNode(anchorNode)) {
      format = anchorNode.getTextFormat();
      style = anchorNode.getTextStyle();
    }
  }
}

return new RangeSelection(
  resolvedAnchorPoint,
  resolvedFocusPoint,
  format,
  style,
);

playground