Lexical 에디터에서 IME(한글 등) 입력 도중 다른 텍스트 영역으로 커서를 이동했을 때, 이전 영역의 포맷(Bold 등)이 그대로 유지되는 버그를 분석하고 해결한 과정을 공유합니다.
문제 상황
특정 포맷(예: Bold)이 적용된 텍스트를 IME로 입력하던 중, 조합을 완료하지 않은 상태에서 일반 포맷의 다른 영역으로 커서를 이동하여 입력을 이어갈 때 스타일이 올바르게 업데이트되지 않는 현상이 발생했습니다.
재현 단계
- Bold 포맷 영역에 텍스트 "가다" 입력
- 일반 포맷 영역에 텍스트 "라바" 입력
- "가다"에서 "가"와 "다" 사이에 "나"를 입력하며 (엔터나 방향키로 조합을 완료하지 않음)
- "라바" 영역의 "라"와 "바" 사이를 클릭하여 커서를 이동
- "마" 문자 입력
결과: 새로 입력한 "마"에 일반 포맷이 아닌, 이전 영역의 Bold 포맷이 적용됩니다.
원인 분석
디버깅
에디터 내부의 Selection 상태를 추적해본 결과, 다음과 같은 사실을 발견했습니다.
- 일반적인 영문 입력이나 조합이 완료된 상태에서는 커서 이동 시 해당 위치의 노드 포맷으로 Selection의
format과style이 갱신됩니다. - 하지만 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이 존재하면 그곳의 format과 style을 재사용하고 있었습니다. 이로 인해 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,
);