본문 바로가기

Frontend

React-Arborist 트리에서 한글 입력 오류 해결

 

react-arborist로 트리 편집 UI를 구현할 때 한글 입력 시, ㅇ 같은 초성이 중복 입력되거나 스페이스가 입력되지 않는 문제가 발생했다.

 

문제 원인

1. 키 이벤트 버블링으로 트리 단축키가 개입한다.

react-arborist는 space, arrow 등을 트리 토글/이동 단축키로 사용한다. 입력 필드에서 발생한 키 이벤트가 상위(Tree)로 버블링되면 공백 입력 대신 트리 동작이 실행된다. 스페이스가 안 찍히거나 포커스가 의도치 않게 움직인다.

 

2. Controlled Input이 부모 리렌더를 유발해 IME 조합을 끊는다.

입력값을 부모 상태에 직접 바인딩하면 매 키 입력마다 부모가 리렌더된다. 가상 리스트(트리 행)까지 재렌더, 리마운트되면서 커서 위치가 초기화되고, IME 조합이 중간에 끊겨 중복 입력 현상이 발생한다.

 

해결 방법

1. 키 이벤트 버블링을 “캡처 단계”에서 차단한다.

입력 컴포넌트에서 캡처 단계로 전파를 막아 트리 단축키가 개입하지 못하게 한다. 

<Input
  onKeyDownCapture={(e) => e.stopPropagation()}
  onKeyUpCapture={(e) => e.stopPropagation()}
  onClick={(e) => e.stopPropagation()} // 행 토글 방지
/>

 

2. 입력은 “로컬 state로만” 관리하고, 커밋 시에만 부모로 올린다.

(기존) 부모가 직접 제어하는 방식은 문제를 유발한다. 위에서 살펴봤듯이, 전체 트리가 재렌더링 되기 때문이다.

// 부모 컴포넌트
const [draftName, setDraftName] = useState('');      // 부모가 입력값을 보관

{isEditing ? (
  <Inputvalue={draftName}                                 // 매 키 입력마다 부모 상태 변경
    onChange={(e) => setDraftName(e.target.value)}    // → 부모 리렌더 → 행 재마운트 → 커서/IME 깨짐
    onKeyDown={(e) => {
      if (e.key === 'Enter') { e.preventDefault(); commitEdit(); }
    }}
  />
) : (
  <span>{node.data?.name}</span>
)}

 

(해결) 로컬 state 별도 관리 및 커밋 시 부모에 반영한다.

function EditableNameInput({
  initial,
  onCommit,
  onCancel,
}: { initial: string; onCommit: (v: string) => void; onCancel: () => void }) {
  const [val, setVal] = useState(initial);
  const [isComposing, setIsComposing] = useState(false);

  return (
    <Inputvalue={val}                                     // 입력은 로컬에서만 관리
      onChange={(e) => setVal(e.target.value)}
      onKeyDownCapture={(e) => e.stopPropagation()}   // 트리 단축키 차단
      onKeyUpCapture={(e) => e.stopPropagation()}
      onKeyPressCapture={(e) => e.stopPropagation()}
      onClick={(e) => e.stopPropagation()}
      onCompositionStart={() => setIsComposing(true)} // IME 안전
      onCompositionEnd={() => setIsComposing(false)}
      onKeyDown={(e) => {
        if (e.key === 'Enter') {
          if (isComposing) return;                   // 조합 중 커밋 금지
          e.preventDefault();
          onCommit(val.trim());
        }
        if (e.key === 'Escape') { e.preventDefault(); onCancel(); }
      }}
    />
  );
}

// 부모는 커밋 시점에만 트리를 갱신한다
const commitEdit = (next: string) => {
  if (!next) { alert('이름을 입력한다.'); return; }
  setModifiedDataWarehousesData(prev => updateNodeName(prev, editingId!, next));
  setEditingId(null);
};

 

입력을 부모 상태에 직접 바인딩해 전체 컴포넌트가 매번 재렌더링되면, 예상치 못한 사이드 이펙트가 발생할 수 있다는 사실을 배웠다.